Skip to content

Decision log — autonomous ownership pass (2026-09-17) ​

Status: Active · Date: 2026-09-17 · Owner brief: "Can you perform as much of the work as possible?" — the owner, fatigued, asked Claude to take over auditing, reviewing, planning, fixing and improving. Product questions that were parked as owner calls are decided here with evidence, built, shipped to preview, and left on the page QA board as one line each for veto. A reversal is an OTA; more than two reversals on one theme become a decision law in the relevant canon.

Only-owner items stay only-owner: a phone in hand (device checklists in device-checklists.md), external accounts (Kakao, PortOne, Apple, GitHub billing — external-blockers.md), and the go-live call.

How to read a line ​

ID · decision · why · rejected · where it shipped. The board entry for the same ID carries the same line as its reply and is marked fixed with the commit; veto by replying on the entry.

Wave 1 — small, unambiguous UX (usability audit Tier 2) ​

  • U-09 · "초대 코드" is a labelled header text action on the 클럽 tab (new HeaderTextAction primitive; the label reuses common.inviteCode) · the bare key icon was the only manual join entry and its label was screen-reader only; a labelled text action is what Toss and KakaoTalk use for secondary header tasks · rejected: keeping the icon with a tooltip (no hover on phones), a full-width row above the list (competes with the list) · shipped: HeaderTextAction (packages/ui), label reuses common.inviteCode. Commit: c36b5111.
  • U-10 · the empty 내 클럽 state leads with 클럽 탐색 and offers 초대 코드로 가입 as its secondary action; the pinned 클럽 만들기 band is secondary while the list is empty and primary once the user has a club · a newcomer is almost always joining, not founding; two rival primaries is the H3 failure · rejected: removing the band on the empty state (founders exist; 밴드 keeps both) · shipped: EmptyState gained secondaryActionLabel/onSecondaryAction; band weight follows isMyClubsEmpty. Commit: c36b5111.
  • U-05 · officials see a labelled "관리" header text action on club detail that opens the existing 관리 hub (ClubAdminScreen); ⋯ keeps 공유 and 나가기 · the hub already existed behind an unlabelled ⋯; a visible label copies the host-tools pattern the audit praised on session detail · rejected: a nav item (the six-tab nav is a ratified place-surface spec), a gear icon (universal but silent for this audience) · shipped: club-detail-screen.tsx 관리 text action beside ⋯ (club.detail.admin-action). Commit: c36b5111.
  • U-25 · the 조 line on member rows is hidden while the club has fewer than two groups; with two or more, one legend line sits above the list ("조는 클럽 순위로 나눈 실력 그룹이에요") · one group means the tag carries no information; a legend on first appearance is the H2 pattern · rejected: a per-row tooltip (hidden gesture) · shipped: showsGroupTag rule + resolveMemberGroupName shared derivation; legend clubList.clubGroup.legend. Commit: c36b5111.
  • U-35 · the 복식 파트너 추천 entry moves from the admin roster (/clubs/[id]/members) to the 멤버 pane every member sees on club detail · it is a personal discovery tool, so it belongs on the surface members open, not the treasurer's management list · rejected: the profile tab (loses the club context the suggester needs) · shipped: row moved to members-pane.tsx, EntryIconBubble shared under clubs/shared. Commit: c36b5111.
  • U-14 · one money vocabulary: 회비 = club dues, 참가비 = session fee, 정산 = the host's split that sets a fee; dues actions 납부 확인 / 면제 / 납부 취소, session-fee actions 입금 확인 / 면제 (the shared verb key said 납부 on session rows against the house style); the session payments screen has one title (참가비) with 입금 현황 and 정산 sections plus one sentence relating the split to the fee and its 1,000원 rounding; the dues roster adopts the session roster's inline verb-pair rows and drops the per-row ⋯ (H4: nothing only behind a menu), 메모 opens from the row itself · rejected: kebab on both (hides the money verbs), three inline actions (does not fit 390px) · shipped: DuesRow inline pills (DuesActionModal deleted), session pills use common.confirmDeposits, title common.fee, costSplitting.splitExplainer, terminology guide ruling. Commit: c36b5111.
  • U-15 · "전체 납부 확인" on the dues roster runs one atomic mark_dues_paid_bulk RPC under RLS with a single confirm naming count and total · twenty client-side mutations (the pickup approve-all shape) would be twenty requests and partial failures on a money action · rejected: per-row only · shipped: migration 00648 mark_dues_paid_bulk (SECURITY INVOKER, RLS-authorised, pgTAP), useMarkDuesPaidBulk, handleMarkAllPaid, clubDues.actions.markAllPaid. Commit: c36b5111.
  • U-18 · the 듀스 toggle is wired into the scorecard rules strip (듀스 / 노애드 badge) and its hint now says players see it in the match rules · it is real player information (advantage vs no-ad); validation cannot use it because scores are stored per set · rejected: deleting the toggle (loses a rule players ask about), inert text · shipped: RulesStrip renders 듀스/노애드 (matchRules.noAd), hint reworded, review step uses the pair. Commit: c36b5111.
  • U-33 · 형식 seeds from club.preferredFormat the way 구성 already seeds from memberComposition, still changeable; the wizard law gains the sentence that a value the same club authored is an answer, a literal default is not · the null-start law guards against fake defaults, not against the club's own choice · rejected: keeping the null start (re-picking the same format on every session) · shipped: create-session-step3.tsx seeds format from club.preferredFormat; wizards.md law amended. Commit: c36b5111.
  • U-12 · cardMeta moves from 13/18 to 14/20 in the one role definition (576 sites follow) · the most common secondary text on every row was below the 14px floor the audit set for this audience · rejected: a new intermediate token (the ladder already has 14), touching micro (numerals only) · shipped: text.tsx cardMeta $4/$4; canon/styling.md and components.md restated. Commit: c36b5111.
  • U-13 · card and row titles wrap to two lines through one shared constant; a lint keeps numberOfLines={1} off cardTitle text unless a comment justifies it · six sites hand-rolled the clip and real venue names ("대치유수지체육공원 테니스장") lost their tail · rejected: per-site edits without a gate (would drift again) · shipped: NAME_LINES (packages/ui/text-lines.ts) at 17 sites, ListRow titleLines, lint no-single-line-title (v0.38.1), AGENTS.md COMP-13. Commit: c36b5111.
  • U-21 · every tier credential is [plain 구력 line, federation line]; TierRangeSelector shows the plain line and keeps federation detail behind one 자세히 보기 · a treasurer describing a casual club met KTA/KATO/KASTA jargon first · rejected: deleting the federation lines (players who know them use them) · shipped: tierCredentials = {plain, details}, TierRangeSelector disclosure tier-range.credentials.details, common.showDetails (was ui.errorDetailsShow). Commit: c36b5111.
  • U-24 · support channels are configuration: SUPPORT_EMAIL plus optional Kakao channel URL and phone from env; the contact screen renders what is configured with Kakao leading · the address was a literal typed twice and a Kakao channel needs an account only the owner can create — the code is ready for the URL · rejected: a placeholder Kakao row (shipped-dead surface) · shipped: config/support.ts (SUPPORT_EMAIL, EXPO_PUBLIC_SUPPORT_KAKAO_CHANNEL_URL, EXPO_PUBLIC_SUPPORT_PHONE), openExternalUrl, contact rows per channel, EXT-15. Commit: c36b5111. Owner action: create the channel and set the env var.
  • U-07b · one first-encounter grammar: a caption under a wizard field the first time a term appears (구성 now; 로테이션 has no bare wizard label), a single InfoHintButton for terms with a sheet (replacing four hand-rolled triggers), plain values on session detail ("20분마다 교대"), 인터클럽 renamed to the app's own 대항전; D-day stays because it is native Korean usage · rejected: tooltips (no hover), a glossary screen (nobody opens it) · shipped: InfoHintButton (packages/ui), glossary.composition caption, sessionDetail.specRotationValue, 인터클럽→대항전 in signals, the Android channel name and the push copy map; terminology guide “First Encounter” ruling. Commit: c36b5111.
  • U-07c · trust-tier thresholds are mirrored from the SQL into a pure rules module with a parity gate against the defining migration, documented in canon, and explained in a TrustTierInfoSheet reachable from every place the tier shows · the push said the tier changed and no screen could say why · rejected: narrating the SQL by hand in copy (drifts silently) · shipped: domain/rules/trust-tier.rules.ts, scripts/check-trust-tier-parity.mjs (in yarn check, AGENTS.md DATA-25), docs/canon/trust.md, TrustTierInfoSheet on own and public profile. Commit: c36b5111.
  • U-06 · 참가비 is editable while the session is open and no payment row exists; the 00404 trigger already enforces that boundary, the form now exposes the field and explains the lock once a payment exists; 종목 / 로테이션 / 모집 마감 stay locked · a typo no longer means cancelling and recreating; the boundary is the one the database already draws · rejected: unlocking the deadline (drives derived recruitment status) and format/rotation (generated rounds depend on them) · shipped: migration 00649 (HINT on the 00404 trigger + participation_fee in the session-update signal), fee row in config-form.tsx (FeeEditModal), sessionDetail.feeLockedByPayments, lock note reworded. Commit: c36b5111.

Wave 3 — design-queue items ​

  • SD-IA (session-detail IA order, parked 2026-09-01) · the shipped order is ratified as canon: hero → StatStrip → live participant bar → refund banners → notice → payment gate → participants → matches → info → host callout → host tools → danger zone, one pinned CTA by state precedence · it already puts identity, the viewer's one action and money before everything else, and no evidence favours a reorder; canon/sessions.md §116 still described a two-surface layout that no longer exists and is rewritten to the shipped order · rejected: info before participants (the hero already carries venue/date/time; the roster is the social proof that drives RSVP) · fix shipped alongside: MatchesSection gated at the call site like its siblings (cancelled sessions no longer double a hairline) · Commit: 747cc80f.

  • LIVE-ANCHOR (duplication audit D11 open item; wireframe live-anchor proposal) · one SessionLiveAnchor band (진행 중 · elapsed · 라운드 N/M · 코트 N) fed by one shared useSessionLiveFacts(sessionId) derivation on the spectator scorecard, the match board and session detail; pinned where the shell already pins (scorecard: above the tabs), first flow child under the header/hero on the other two · the three surfaces signalled "live" three different ways and none stated elapsed, round or courts; the formatter written for it had zero call sites · rejected: the two stacked half-height bands (two seams for one fact), a new DetailShell pinned slot (a shared-primitive change for one band; revisit if the owner wants it truly fixed on match board) · Commit: 747cc80f.

  • U-32 (create-club quick start) · step 1 gains a "기본 설정으로 빠르게 만들기" toggle (off by default) that seeds real values — 구성 혼합, 형식 복식, 실력 무관, 균형, 게스트 허용, recruit target from the schedule-derived suggestion — and the wizard's step sequence skips 스타일/인원/회비/소개 (name → where → when → review, four screens); the review keeps every seeded row editable with a 기본값 badge; ClubPlanningSection gains the missing maxMembers editor so "fill in later from 클럽 설정" is true for all three axes · nine screens and two arithmetic receipts were the H8/H12 failure, and every skipped field already had a submit-time default (create-club-submit.ts) plus a settings editor except 인원 · rejected: a separate short wizard (two wizards to keep in sync), quick as the default (item #13 wants card-critical answers deliberate; the toggle is the deliberate act) · Commit: 747cc80f.

  • REGIONS (multi-region settings entry, courts-derived vs explicit) · courts stay the single source of truth (owner ruling 2026-08-08) and the derivation moves server-side: a trigger on court_venues recomputes the club's primary base and club_play_regions (first distinct district = primary, up to three additional) whenever a club's courts change; the wizard's submit no longer overwrites each created court's own district with the club's; club settings shows a read-only 활동 지역 line with a link to 코트 관리 (a manual district field remains only for a club with no courts) · the junction was populated once at creation and never again, a club that moved courts kept stale regions, and an explicit region picker would reintroduce the 활동 지역 UI the owner removed · rejected: an explicit picker (two truths), client-side re-derivation on every venue edit (misses edits made elsewhere) · Commit: 747cc80f.

  • PHOTO-ROW (사진 업로드 auto feed row product definition) · one row per member per day: "OO님이 사진 N장을 올렸어요" from that member's approved club_media of the day, thumbnails on the shared FeedPhotoStrip, tap opens the album, under 전체 only, capped at three recent like the match row, supplied by the club-detail bundle RPC (no post row, no trigger) · the codebase already names MediaUploadFeedRow and the 전체-only rule for auto rows; grouping by day matches how albums are browsed and avoids five rows for five photos; the gallery picker gains multi-select so a batch is one action · rejected: a system post per upload (five rows per outing; contradicts the stated design), a trigger-batched post (time-window upserts in SQL for a display concern) · Commit: 747cc80f.

  • VENUE-RESIDUALS · the 경기 tab's 테니스장 찾기 pane is the explore entry point; /courts stays as the web and deep-link address of the same content; the 미확인 pick-mode question is closed by the 2026-09-06 canon (no trust filter, no 미확인 badge) — the vestigial verification: 'all' parameter is deleted and canon states that the inclusive policy covers pick mode · rejected: a second in-app entry to the same list, an intent-conditional verified-first default (would reverse the owner's own ruling) · Commit: 747cc80f.

  • CARD-FOLDED (2026-09-18, owner chose "the first" of two options — the glyph-fact grammar over a chips-only compaction; /wireframes/card-continuity) · SessionCardV2 is the session-detail hero, folded into the three lines its square tile is bound to: L1 venue (one line — the aspect-bound tile squeezed siblings on a second line, the U-13 exception in text-lines.ts) + ONE chip in the hero's titleBadge seat (SessionLeadBadge: the strip's lead, else the standing; standing when live) + bookmark; L2 the hero's 📅 fact at GlyphFactRow density="inline" with the time folded in (date + time are one when-phrase on the card); L3 the hero's 🎾 format·style fact (sessionTitle()) + TierRangeBadge density="inline" (bare dots + numbers, no NTRP prefix) | fee; closing line = the meter alone. Weather leaves the card (the strip's rain chip + the hero's forecast cover it); format/style chips and the closing-row lead+standing pair are retired (SessionStateInline deleted, format-label.ts deleted). Web parity: CSS cannot transfer a stretched cross size into aspect-ratio, so the card measures its column and hands the tile a definite square on web. Rejected: the 3-line no-closing-row variant (fee + meter cannot share L3 at cardMeta 14 on a 360dp column — measured, not guessed) and a chips-only compaction (no grammar continuity with the hero).

  • EVENT-TYPE (2026-09-18, session-event-type-plan.md slice 4) · sessions.event_type is its own TEXT+CHECK column (SessionEventType, five values) with a dedicated parity gate, never a Postgres enum and never folded into an existing field · the domain already had three unrelated kinds (SessionInfoModel.kind, StrategyRegistry.kind, seasons.kind) so that name was unusable, and a real Postgres enum can't add a value without a migration lock, blocking league/interclub from ever growing their own structure later · rejected: naming it kind (three collisions), a Postgres enum (irreversible — TEXT+CHECK already ships elsewhere, 00500), a separate tab for 대회/리그/대항전 before their info model actually diverges from regular/pickup (Playtomic only forks navigation once the flow diverges; premature here) · shipped: wizard 경기 유형 picker (create-session-step3.tsx, club flow only, DropdownChip standalone), review row + draft persist/resume, detail info-section fact row, terminology guide + canon/sessions.md.

  • QUICK-CHIPS-REVERSED (2026-09-18, owner veto #1 of the autonomous pass) · the three always-visible quick chips (날짜 · 종목 · 경기 유형) in PickupFilterBar's collapsed slot are removed the day they shipped — "we already had the search bar + filters; the chips just happened to be duplicates of the contents inside the filters" · what stays: the panel's 경기 유형 row, the card-omission law (omit now derives from the PANEL's active filters — a set 날짜/종목/경기 유형 row still drops its fact from every card), the zero-result EmptyState, DropdownChip standalone (it pre-dated the chips — wizard fields) · what goes: QuickFilterChips, WFQuickChip + the kit bar's quick prop, the session-filters board's chip frames · lesson for the decision law: a control that restates a control one tap away is chrome, not a shortcut — the 6–8-chip Baymard threshold measures visible controls, not distinct dimensions · next: the owner asks for the date to become the FRAME on every session list (date sections) rather than a per-filter omission, and for filters on home's 주/월 date selectors — researched under DATE-SECTIONS.

  • DATE-SECTIONS (2026-09-18 pm, session-event-type-plan.md D10–D13; /wireframes/session-list-frame) · every list of session cards is grouped by day under SessionDayHeader (title formatDate, right-pole 오늘/내일) — the date leaves the card for good; the card is 2+2: L2 ⏰ time · 🎾 format(+type), L3 🎯 tier | fee (D11); omit narrows to format/type and OMIT_DATE is deleted; a single-day surface (home 오늘, the live stack) is its own frame · every agenda product (Google Calendar schedule view, Apple Calendar list, Todoist Today/Upcoming, Luma) makes the day the frame and leaves only the time on the row; M3 list guidance and Airbnb's card grammar keep supporting text to one quiet line, and the owner named the three-fact L3 as the cram · no filters on home's 주/월 (D12 — owner confirmed 2026-09-18 evening: "no filters perhaps"): home lists the viewer's own 2–12 sessions per period; Strava and ClassPass keep filters where lists are long and separate from the date navigator — if ever wanted, one 필터 toggle at PeriodNavPanel's right pole opening the same panel, never a chip row · taxonomy stays five (D13): USTA/LTA/KATA and Playtomic/Spond reduce to social play · club session · tournament · season league · club-vs-club; box leagues/ladders are draw or season structures, 게스트 데이 is a field we have, lessons are a separate product in every app and absent from our schema · rejected: GroupedFeedList (un-virtualized RN SectionList — home's row-union FeedList is the precedent), a segmented 종목/유형 control above every list (restates the panel, the quick-chip mistake again), keeping the date on the card under a day header (restates the frame) · also: the 🎯's clipped bottom on Android was a clipping ancestor one text line tall around a GlyphSlot (overflow="hidden" on the inline GlyphFactRow and the card's L3) — removed, rule documented on the primitive: no clipping ancestor around a glyph row; horizontal containment is minWidth 0 + flexShrink + numberOfLines. · owner follow-up the same evening: 이번 주/이번 달 예정 titles removed from home's 주/월 panes (dates are enough) and ONE list rhythm ratified across pages — card→card 12, day boundary 16, header→card 8 — with the header owning its 4px top seam and the host owning nothing after a header (itemSeparator reads leadingItem only: RN FlatList never provides trailingItem); the 클럽 일정 panes had stacked cards with NO seam (itemSeparator={null} since phase 9b) — same rhythm now. · SEAM-HOSTS (2026-09-19, owner: "consistent spacing across pages"): audit found the title→item seam at 8 / 10 / 12 / 20 px depending on the host — three primitive defects (GroupedFeedList's default section separator rendered after a header on top of the header's own 8; dense owned no bottom seam; block min-height 32 vs 28 line) fixed at the primitive (SectionSeamSeparator default, seamOwnerSeparator, dense paddingBottom, block min-height dropped; SectionHeader leading slot for the 조 tier dot); never a per-site null again. · FEE-OFF-CARD (owner 2026-09-19): "no session is free, or you've already paid upfront" — the fee leaves the card; L3 is the 🎯 tier band alone; cost drops out of the card projection; detail + 요금 filter keep it. Caveat noted, not disputed: on 번개 찾기 a stranger's pickup fee is the one per-session cost that varies — the panel filter and the detail cover that path. · TIER-DOTS-OFF + ROW-RHYTHM (owner 2026-09-19): the tier-colored dots on TierRangeBadge were noise ("unless we have clear graphical improvements") — text-only now on every surface (session card L3, club card pill, public profile), the legend sheet alone keeps colour; club pages' title→item seam was the ROW side: MemberRow padded $3 on top of the header's 8 (→ SEAM.item, the ListRow rhythm DuesRow already states) and the 회비 header's sm button outgrew the title line (→ xs); the same rule swept every hand-rolled peer row (paddingVertical $3 belongs to multi-component block rows only). · EMOJI-LINE-BOX (owner device QA 2026-09-19, second report): the clip survived the parent fix on the phone because Android draws a Text only inside its own line box and Samsung's emoji font is taller than Noto — emojiLineHeight(fontSize) = fontSize + 8 is the one rule (GlyphSlot 12/20, TierBadgeChrome, identity chips via GlyphSlot); the emulator cannot reproduce vendor fonts, so this one is verified by construction + the owner's List 16 row 3. · VISIBLE-EDGE SEAM (owner 2026-09-20, third club-pages report): the title→content seam is 8 to the content's first VISIBLE edge — over padded rows the header is flush (SectionHeader/SectionBlock flush), since RowList/FactList already trim the first row and only the club lists that bypass them stacked 8+8; three-line rows top-align their leading avatar (M3). Measured on device: 멤버 23→15, 회비 28→11, card reference 12.

  • SHARE-CARD-HERO (2026-09-20, owner: "I want the hero to become a sharable session card… currently it's just a collection of badges"; /wireframes/session-share-card, docs/architecture/session-share-card-plan.md S1–S5) · the session detail's hero is SessionShareCard — the folded list card opened up (2:1 map band, club eyebrow, venue + RecruitmentStatusBadge, 📅·⏰ / 🎾·🎯 inline facts, meter), a projection of SessionInfoModel through buildSessionShareModel (new share surface) that ALSO renders the web share page, the 1200×630 OG image (tile+column) and the Kakao FEED template (image + venue + 2-line description + 참여하기) — share = link, the link unfurls to the card · the page stops restating it: bare header, StatStrip = 내 상태 · 내 참가비 · 신청 마감, 정보 without 시작 시간/참가비, the venue block without its second map, 공지's 📣 through GlyphSlot · research: Luma/Meetup generate the share image from the hero's own asset; Kakao feed takes a per-call image; Wallet passes put place · when · status on one face; the previous OG card was a generic green card and the Kakao rail a text-only custom template · rejected: on-device screenshot share (needs react-native-view-shot + expo-sharing — native, no OTA — and bakes the sharer's 🔒/standing into what strangers receive), chip density on the hero, faces on a forwardable image (PII), the custom template (console-reviewed per redesign). · SUPERSEDED the same evening by PAGE-NATIVE (owner: "both A and B, since they use components from the summary card, don't seem to flow visually… we don't even have to stick to the stat row"; /wireframes/session-hero-quiet D): the detail page uses its OWN components from the top — MediaHero (eyebrow club + venue title, no badge, no facts) → a title-less FactList (일시 · 종목 · 실력 · 참가비 · 신청 마감) → 참석 (faces, header meta 모집 중 6/16) → 호스트; the RN SessionShareCard, the ViewerStatStrip and the title chip/meter/glyph rows leave the screen; the share artifact keeps the share model (OG image, Kakao) as a subset of the page. Law: card ↔ detail share a MODEL and a fact ORDER, never components. Research: Meetup/Eventbrite/Luma live fetches, Airbnb one-line title, Google Calendar title/when/where, Toss one message per screen; Dice/Partiful/RA bot-blocked. Owner asked "more graphical?" — answer: the graphics that carry information are already there (map, faces, host avatar); a NTRP scale under 실력 is the one candidate, held for device judgement. · FACT-ROW GLYPHS (owner, same evening): five label/value rows with short values gave the eye no anchor at the row start — FactRow/FactList render SpecItem.icon (a slot SpecList had and FactList ignored) in the GlyphSlot; the detail's top rows lead with the list card's own glyph for each fact (SESSION_FACT_GLYPH + fee/deadline/approval) — card ↔ detail continuity is now facts, order AND glyph, still never components; the compact (left-aligned value) variant is the next lever if the gap still reads loose on device.

  • FEE-ON-CARD + MY-STATUS (2026-09-21 00:40, OWNER decision made in a Gemini/Antigravity session — commit f446f97f, OTA 494b7855; logged here after the fact because that session updated no canon, no kit and no board) · the list card's lead chip leaves L1 (the venue title owns the line) for the closing line, where it sits left of the meter; a browsing viewer (neither host nor confirmed) sees the fee there as quiet meta text (SESSION_SURFACE_PROJECTION.card lists cost again); the detail's fact block gains a 👤 내 상태 first row (host / 참석 확정 / 예비 badge) for a participating viewer · this REVERSES FEE-OFF-CARD (2026-09-19) — reversal #2 on the session card after QUICK-CHIPS (#1); one more on this card and the rule says a decision law gets written · the docs/kit/wireframe drift the Gemini pass left behind was closed on 2026-09-21 (canon sessions.md composition, WFSessionCardV3, this line).

  • GEMINI-AUDIT TRIAGE (2026-09-21 — shipped as commits eebd81d4 + 2454854d, OTAs 8ad45cd8 + 80b7b66e, device List 18; owner: "see if the points it makes warrant further investigation and proper implementation" — ~/.gemini/antigravity/brain/930b6a08…/session_detail_and_reachable_pages_investigation.md) · every claim was verified against the code before anything was built; verdicts: · ROSTER-WALL (P0, confirmed, fixed) — ParticipantsScreen was walled behind canManageSchedule while 전체 보기 / 외 N명 / 참가자 관리 (host OR schedule manager) all route there, so every plain member and every non-admin pickup host met 접근 권한 없음; reading a roster is RLS's decision, not a client capability — the wall is gone, viewers get 확정+예비 with rows navigating to the profile, canManageSession adds the manage title/subtitle and the 취소 section, canRemoveParticipant (club admin/owner — rsvps_update 00080; the host's 00426 policy is availability-scoped by 00433, so a host may NOT remove) and canAdmitWaitlist (host OR admin, 00478) add the row actions · RULES-GATE (P1, claim corrected, fixed) — Gemini said the pickup host was locked out and proposed widening the client gate to the host; the server RPC update_session_rules (00147/00152) refuses a non-admin host, so that fix would have traded a wall for a server error; the real drift was the other way — the client (canManageClub) was STRICTER than the RPC (owner/admin/match_director), hiding the editor from 경기이사, and the tournament chooser's 경기 시간 hint leaked every chooser-eligible viewer into the wall; fixed with ONE capability table (session-capabilities.rules.ts, each entry naming the migration it mirrors; useSessionCapabilities) read by the host-tools gate, the rules screen and the chooser; host authority over rules stays a server/product question (see follow-ups) · HERO-HINT (P1, confirmed, fixed) — the map band was the only path to the venue page with no resting cue (affordance law); MediaHero mediaHint renders MarkerPill 장소 보기 bottom-left on the band (VenueCard's own tile-hint grammar; MarkerPill moved to @twomore/ui) · ROSTER-ROWS (P1, confirmed, fixed) — roster rows dead-ended while the same page's host card and every friends/match/DM row navigate to routes.profileDetail; rows now navigate, and ListRow learned trailing={marker} chevron (a content marker beside the one cue) so the two hand-rolled badge+chevron compositions (AdminNavRow, SettingsRow) collapsed into the primitive · INFO-GLYPHS (P2, confirmed, fixed) — two FactLists on one page, one glyph-led and one bare, was exactly the "unsynchronized primitives" the owner rejected on this page; ruled: the glyph belongs to the FACT, not the block — 정보 rows carry 🏷️/🏅/📈/🔄/📋 and 날씨's glyph is the live condition (value text-only) · DEADLINE-PAST (P2, confirmed, fixed) — 참석 마감 read "9/20까지" the day after; formatDeadline gained its past form (M/D 마감됨, 오늘 h:mm 마감됨, and the -0 ceil case a deadline seconds ago produced) for every consumer · NOT warranted: header-menu host shortcuts (a second home for 호스트 도구 rows — one home per action), cancel reason in the terminal banner (no such field exists on the entity or in any migration), venue edit after creation and a rejection note for applicants (product scope, not drift), the host dashboard's 일정 취소 (a host console legitimately repeats it), use-session-detail-model.ts at 514 lines (under the 600 gate) · AUTOLINK (P2, confirmed, built the same day as wave 2): URLs a 총무 pastes into a 공지 (오픈채팅, 네이버 예약) were dead text — one primitive, LinkifiedText (@twomore/ui, linkify-it 6.1.0, pure JS, fuzzy bare domains on, emails off, nested Text onPress = the RN link idiom) bound to openExternalUrl as AutoLinkText (@twomore/app), now the ONE body renderer for 공지, DM bubbles (inverse link ink on the outgoing bubble), post bodies and the club 소개; phone numbers stay a follow-up · follow-ups, not built: host authority over match rules (server-side, a migration + product call — the host already holds score authority, 00432); 경기 방식 and ELO rows both derive from kind.isFriendly (one fact, two rows — a merge candidate); session-detail-screen.tsx sits at 599/600 lines (the next touch splits it); VenueCard's map zone (ZONE 1, heroOverlay, its bottom-right pill) has no mount left — its only caller passes showMap={false} — a stale branch to delete or fold into MediaHero · found while verifying on the emulator (P1, not built): the PUBLIC PROFILE (/profile/[userId], public-profile-screen.tsx) renders nothing below its StatStrip on Android — the native tree (uiautomator) shows the scroll content ending ~590px after the strip with NO child views for the 플레이 section or the 친구 well, and the strip's tappable 신뢰 등급 cell stretched to that bottom (Button [812,673][949,1403]); the venue detail's identical FlatColumn lead={StatStrip} composition renders fine, and jest (kit mocks) passes, so it is a native-only render loss on that screen — the same "space allocated, children unpainted" signature as the 2026-09 Fabric recycling bug. Every roster row now lands on this page, so it is next in line.

  • PUBLIC-PROFILE-ANDROID-FIX + SESSION-DETAIL-HEADER (2026-09-21 — shipped as commit 64b006ae, OTA 59f028ee, device List 19) · resolved Android Fabric native render loss on the Public Profile (/profile/[userId], public-profile-screen.tsx): in StatStrip, StatTile now passes flex={false} and Pressable has width="100%" alignItems="center" inside CellRow's column (YStack flex={1}) so tappable stat cells retain their natural intrinsic height; in public-profile-screen.tsx, removed scroll={false}, duplicate ErrorBoundary, and nested ScrollView, restoring native DetailShell scroll ownership and pinned footer padding; in session-detail-screen.tsx, extracted SessionDetailHeaderRight (session-detail/session-detail-header-right.tsx) keeping session-detail-screen.tsx safely at 586 lines (ARCH-10 compliance).

  • OPTION-A-ALERTS + ROW-INSETS + BADGE-TYPOGRAPHY (2026-09-22 — commit ab9d3790) · session detail alert hierarchy reorganized to Option A: elevated actionable transactional alerts (PaymentGateBanner hold countdown and RefundBanners host obligations) above SessionHeroSection with rail padding (paddingHorizontal="$4", gap={SEAM.block}) alongside terminal banner so time-sensitive holds are never demoted below the fold; in <FlatColumn>, NoticeSection is ordered immediately below SessionFactsSection and LiveParticipantBar for clear visual rhythm (Core Facts → Host Notice → Roster → Matches → Info); fixed selectable component hover/press layer in ListRow and StatStrip by adding 12px horizontal padding, negative margin compensation (-CONTENT_INSET.block), and $3/$2 rounded pill borders; fixed badge text shrinkage at primitive and presentation levels by elevating Badge and all status badge wrappers (status-badges.tsx) to size="sm" (12px badge role) default, preserving xs for compact count bubbles.

  • SCREEN-AUDIT (2026-09-23, owner: "apply the same rigor to other screens … bring back the report that will be analyzed, researched, planned and implemented") · docs/design/screen-audit-2026-09-23.md — every screen captured on Android (78 deep-linked routes, native layout trees for measured spacing) plus three code sweeps; findings by class A–I with instances, a top 20, a per-screen index and a proposed wave plan (W0 correctness & roots → W5 process). Two P0s found on device: the payment banner shown to hosts, non-joined members and cancelled sessions (sessionPaymentGateState treats "no payment row" as reserved), and the ELO trend chart plotting per-match deltas. The scroll-hint pill root cause (viewport height reset on item-count change) was found only by combining the device tree with the code; both code sweeps had ruled it out. HEAD was made green first (102980d0, OTA a39f0311) so the audit ran against shipped code. Nothing in the report is fixed yet — next phase is research → plan → build, owner decisions listed per wave.

  • OPTION-A-ALERTS follow-up (2026-09-22 01:23 — commit 85a12160, logged 2026-09-23; DOC-6) · the same night's Option A (alerts ABOVE the hero) was moved again 45 minutes later: the alerts block became the FlatColumn's first child, BELOW the hero title and above the facts, and NoticeSection moved above SessionFactsSection. Device List 20 rows 1 and 3 described the superseded layout (corrected 2026-09-23). The shipped order is the one in canon sessions.md "Detail-screen IA".

  • IMPROVEMENT-PLAN (2026-09-23, owner: "fix the outstanding and explore issues on codes that gemini worked … audit, review, plan, implement and review a plan to improve the front and backends and docs") · docs/design/improvement-plan-2026-09-23.md — five audits (screen, Gemini code review GM-01…23, backend BE-1…16, docs DOC-1…18, benchmark research) → decisions + waves W0–W5. Decided with evidence, veto-able: PAY-NONE (no payment row = no obligation; one isSessionPaymentOutstanding rule; never on a cancelled session) · ELO-PATH (the trend chart plots the rating path) · SCROLL-PILL-RETIRED (no mainstream precedent on static lists) · ROW-BLEED (a tappable ListRow bleeds at a section edge instead of ignoring it; RowEdgeContext carries edge through wrapper rows) · HOST-TOOLS-ONCE (the body section only; ⋯ = share · group chat · 문의하기) · CONTACT-HOST (문의하기 stays in ⋯; the host is the roster's first row) · LENS-LABEL (경기 tab lens = 테니스장) · EMPTY-ONCE (an empty section renders nothing; one line for a mostly-empty new-player page; no ghost content — Strava/Instagram consolidate, Apple's don't-hide rule is about tab bars) · ACHIEVEMENTS-REAL (no 준비 중 entries) · BADGE-EXCEPTION (a row badge only when the row differs from its list) · CLUB-ELO-LABEL (club pool = 클럽 ELO; DEFAULT_ELO replaces the ?? 1200 fallbacks) · ONE-ATTENDANCE (attended ÷ sessions held since joining, pooled, one RPC) · HOME-TODAY (일 = my clubs' sessions today + my RSVP'd pickups; the Spond/TeamSnap closed model). Proposal only, owner design queue: D3 (club cards without a real photo drop the default court band).

  • W0 + GEMINI FOLLOW-THROUGH (2026-09-23 — built from IMPROVEMENT-PLAN; commits 6b632dba fa3e392f 47ef2772 c10fd05a, OTA 7469af8a, device List 21) · A1 P0 payment banner (PAY-NONE; the club card's outstanding-fee cue had the same bug through viewerPaymentGate) · A2 P0 ELO chart (ELO-PATH; formatEloDelta replaces three hand-rolled signs) · D1 scroll pill removed with its props, i18n key and ui-strings bridge · E1/E4 (ROW-BLEED; invariant test: static ≡ tappable content inset on every edge; flush dropped from the two RowList hosts it had been added to) · G1 (LENS-LABEL) · H1 CTA → 번개 찾기 · C9 one context line on timeline rows · F2 start time on club home rows · GM-02/03/04/06/07/21 (HOST-TOOLS-ONCE, SessionAlertsSection gated by the model, inquireHost in the pure share builder, the participation title as one i18n template, the default roster row label, one 내 상태 item). The screen test's mocked model now runs the real payment rule — the hand-copied formula is how A1 had passed. Removing the pill exposed four hand copies of the list slots across native/web: GroupedFeedList is now ONE implementation for both platforms (feed-list/grouped-feed-list.tsx), with one slot rule (resolveListSlots) and one scroll-handler composition (useScrollJankHandlers) — clone baseline 1018 → 891 lines.

  • W1 BATCH 1 — subtraction by rule (2026-09-23, from IMPROVEMENT-PLAN; commit fa3e392f, OTA 7469af8a, List 21) · C1 the roster badges only the exception (호스트, 예비) — the 참석 title already states the default, so no 참석 확정 on every row, nor on a completed session (A9's roster part) · C8 home is titled 홈 like every other tab; the 일간/주간/월간 브리핑 title restated the pills and its three keys are gone · B6 ACHIEVEMENTS-REAL: the gallery lists what can be earned today plus what the player holds, and the preview's N/M counts the same set (isAchievementShown); the badge's 준비 중 state retired — this touches an owner-parked area ("achievements later"): display only, the unlock engine stays parked, the hidden types come back on their own when hasProductionUnlockPath flips; veto restores the 준비 중 rows · B10 the admin snapshot shows only non-zero cells; a quiet club reads one line (지금 확인할 일이 없어요) · D4 클럽 만들기: a newcomer keeps the secondary band (U-10: join first), a member with clubs gets a "+" header icon beside 초대 코드 instead of a pinned band on every visit.

  • DEVICE QA of OTA 7469af8a + TAB-PARAMS (2026-09-23, Android emulator) · verified: no 입금 필요 banner for a non-payer or on a cancelled session (A1), the ELO chart rises for two wins (A2), roster rows carry no 참석 확정 badge (C1/A9), the host's ⋯ holds only 일정 공유 (HOST-TOOLS-ONCE), 홈 title (C8), 테니스장 lens in full (G1), 업적 4/11 with no 준비 중 row (B6), admin strip 12 회비 미납 + 1 신고 only (B10), 클럽 "+" with no band (D4), no scroll pill on a one-row list (D1) · found and fixed: TAB-PARAMS — the empty 일 card opened 내 경기, not 번개 찾기: /activity and /clubs are alias routes whose bare <Redirect href="/(tabs)/…"> dropped the query string, so EVERY ?tab= opener (routes.activityPickups, routes.discoverClubs — the nearby-pickups card, 클럽 둘러보기) landed on the tab's default pane. One TabAliasRedirect (packages/app/src/navigation) forwards the params for all five tab aliases; check:screen-blueprints now fails a pure-redirect route that redirects bare to a tab root (dropsTabParams, route-tree test) · also found, queued: the session 경기 section repeats the same 🥈 medal on every player (C6 class → sessions lane); the club 테니스장 관리 list's first row sits flush under the header (F6 venue rows); owner question RSVP-DEADLINE-MEANING (plan §2).

  • GATE-ONE (2026-09-23, plan W5 I5) · yarn check is the one gate for done, pre-push and CI: docs, check:static (every static gate), typecheck, test, lint:strict, agents-md, stale-claims. Before this, pre-push lacked the static gates and yarn check lacked lint, so Gemini's 3c18d142 pushed green while yarn check was red · AGENTS.md AOR-10 single writer: one agent session writes a tree at a time; handoff = pushed + OTA'd + green.

  • ROUND-4 DECISIONS (2026-09-23, plan W1/W2; boards follow) · ONE-CLOCK 24-hour "HH:MM" is the one clock — session times and ranges already use it; the 12-hour 오전/오후 form (deadlines, late-arrival hint, wizard deadline summary) retires · PERSON-ROW one PersonRow on ListRow for every person list (photo then initials, one sub, exception badge, optional action; the partner suggester shows the face, not a sparkle) · EXCEPTION-LEDGER 회비 관리 states the amount and due day once and sections rows by state with one pill on an unpaid row; 출석 확인 rows default to 참석 with one control to mark 불참 · KIND-GROUPS the 할 일 band keys admin work by kind; a kind in one club is that club's row, a kind in several clubs is one expandable row; every filter chip counts rows · EMPTY-ONCE on profiles empty sections render nothing; an all-empty profile shows one line; the own profile keeps one 친구 찾기 prompt.

  • ROUND 2 — one match row, records subtraction, strings out of code (2026-09-23, plan W0b/W1/W2; commits cf09ac60…b5a4ea41, OTA 0aac6e5d, List 22) · F3/GM-16 one MatchHistoryRow in @twomore/app renders records 경기 기록 AND the public profile's recent matches (tappable; every row shown is a session the viewer can read — match history skips unreadable sessions) · GM-09…17 public profile: honest 총 경기 (never sessions attended under that label), DEFAULT_ELO, FORMAT_PREFERENCE_ELO_GAP, one formatPercent, no duplicate 클럽 전체 보기 link, club row in a RowList (E2 seam), no 이미 친구예요 well · B4 no ghost "— — —" record rows below the threshold (call-site gate) · C4 BADGE-EXCEPTION on leaderboards: the tier badge only where the tier run starts · BE-1 the day detail's 최종 레이팅 reads one pool (doubles unless the day is a single format) · F7 part the format breakdown keys on SessionFormat · DATA-7 mutation toasts, adapter error messages (English diagnostics; one code→message map at the boundary), Android channel names and domain labels moved into i18n; no-korean-string-literal now covers adapters/ + domain/, toast keys and *Error constructors, with a per-file i18n-exempt: marker stating why (proper-noun tables only) · follow-up queued LOCALIZED-TEXT: matchmaking strategy and rotation-preset copy still ships as {ko, en} literals in domain resolved by three hand-written getLocale() ternaries — moves into the i18n registry keyed by id.

  • ARCH-BLIND (2026-09-23, found while wiring TAB-PARAMS) · the ArchUnitTS fitness suite had been checking nothing: archunit 2.1 read the raw tsconfig JSON (never followed extends), so its path resolver got undefined options, every import threw and was skipped — zero edges; and FileInfo.content is read relative to the CWD, empty when yarn test runs from packages/app — every content rule matched nothing. ARCH-1…4, hex/1…5 and ui/1–2 all passed blind. Fix: archunit 2.5.4 (follows extends), content read from the repo root, and two canaries (a known relative edge, a known @/ alias edge, real source text) that fail if the suite goes blind again. Seeing again, it caught: the ARCH-2 deep-import rule counting the public index.ts (498 false hits — rule fixed), ARCH-4 ×2 (TabAliasRedirect, the nav-perf hook — both now behind app-router.ts), hex/4 ×16 (presentation importing adapters: the idempotency classifier → lib/idempotency.ts, the owner-claim failure vocabulary → domain/errors).

  • BATCH-1 REVIEW (2026-09-23, adversarial reviewer on 6b632dba…47ef2772) · P1 the club list read a loading list as "no clubs" and flashed 클럽 만들기 at members on a cold cache → membership is unknown (null) until both lists are known, a guest invite counts by id; tests for loading/offline/error · P2 the config barrel re-exported the visual-accents module (~30 icon imports) into every import { t } from '@/config', including the web OG edge bundle → no longer re-exported · P2 the pressable row's vertical-only hitSlop is now documented (the horizontal bleed is its sideways reach).

  • ROUND 3 — one number per metric, one label per rating, sessions subtraction (2026-09-23, plan W1/W2/W3/W5) · ONE-ATTENDANCE built: 출석률 = attended ÷ club sessions held since the member joined, from one server source (private.club_attendance_since_join, migrations 00659–00662); the club-wide figure is pooled over active members on both admin screens (never a mean of rates), the member peek uses the same definition, and the label states the window (가입 후 출석률) · BE-4 one 7-check profile-completeness rule (server's checks: approved AND public media), parity-gated to the SQL (check:club-profile-completeness-parity) · BE-7 the join conversion sits under the join-tap cell and renders only when numerator ≤ denominator; every growth source key has a label (exhaustiveness test) · CLUB-ELO-LABEL built: one ratingScopeLabel; club tables, podiums and the regroup preview read 클럽 ELO; DEFAULT_ELO replaces every ?? 1200/?? 1000 and no-hardcoded-elo-fallback keeps them out; the activity simulator's own copy (and its ELO_1200 threshold, 1300 → 1200 like the server) is pinned by check:elo-parity; the ranking's ELO column widened to fit · BE-5 the profile streak reads 최장 연승 (it is the longest streak — the same words 기록 uses) · B3/B7/B8 no 경기 section before a draw, no empty participant groups, one calm line on an empty pickup dashboard and its duplicate 번개 취소 removed (the detail's 위험 구역 owns cancel) · C6 a round's rows don't restate its round or a shared format; the medal shows only when players' tiers differ (the "same 🥈 on everyone" was every player sitting in the wide silver band) · A9 a challenge past its end date reads 종료 (deriveChallengeDisplayStatus + ChallengeStatusBadge); consent expiry is date-only · LOCALIZED-TEXT strategy and preset copy moved into the i18n registry keyed by id; one matchmaking-copy module reads it (no locale ternaries, no casts); a dead duplicate preset table deleted · consolidations found on review: one local-date formatter for the domain (four hand-rolled copies), club status badges split out of status-badges.tsx under the 600-line cap (the badge lint rules exempt the canonical status/ folder, not one file name).

  • ROUND 3 — backend hygiene (2026-09-23, plan W5) · BE-13 deleting a DM recomputes the thread's inbox preview (00663) · BE-12 DM get-or-create is race-safe (ON CONFLICT on the partial direct-pair index, 00664) · BE-14/15 dead is_authenticated / decay_confidence_for_long_inactive dropped after a caller grep and a pg_depend check (00665) · BE-11 static-map declares verify_jwt = false in config.toml · BE-10 migration numbering gaps noted · pgTAP the suite is green again locally (141 files, 3,235 tests): seven files counted rows beyond their own fixtures and broke on local QA data — scoped to their fixtures, no assertion weakened.

  • ROUND 3 — boards (2026-09-23) · installed empty-once and notification-groups (decided), folded EXCEPTION-LEDGER into club-ops' 회비 and 출석 phones; home / club-list / social / activity / session-list / session-hero boards updated to the shipped UI · WIREFRAME-GROUNDING per board: the drift baseline was keyed by source file, so re-grounding one board silently accepted every other board citing that file (three profile boards were hidden this way today) — now keyed per board and source, with legacy file-level entries as fallback; the silenced boards show drift again until verified.

  • ROUND 3 — home 일 (2026-09-23, plan W4 H1 / W5 / W1 B5) · HOME-TODAY built: 일 lists today's sessions from the member's clubs — answered or not — plus their confirmed sessions, deduplicated, host first · PICKUP-TODAY (found by the home lane): "my confirmed upcoming sessions" was derived from the clubs' upcoming list, so a confirmed PICKUP (no club) could never appear on 일, the home recommendations, the period recap or the calendar dots — it is now derived from the viewer's own RSVPs (the same id query 내 경기 uses; one cached request), with a pure, tested selector (selectRsvpdUpcoming) · test infra one kit-wide mock for react-native-spotlight-tour, so the real HomeTodayView is render-tested (loading / empty / live / scheduled / recap; the scheduled cases fail if the fix is reverted) · B5 an empty period's recap is one calm line, not a ghost donut.

  • EDGE-AUTH-DECLARED (2026-09-23, found while redeploying simulate-activity after the DEFAULT_ELO fix) · the gateway JWT setting of 16 of 18 edge functions lived only in the deployed project, and CLAUDE.md documented functions deploy <name> --no-verify-jwt for every deploy — following it would have switched gateway JWT checks OFF on user-facing functions (export-user-data, send-push, search-venues…); deploying without it had already broken static-map once. Every function now declares verify_jwt in supabase/config.toml, matching the live project (supabase functions list -o json); check:edge-function-auth (with a canary test) fails on a function without a declaration; the documented command no longer passes the flag.

  • ROUND 3 SHIPPED (2026-09-23) · commits 3917acea…4e5f91f4, OTA faecdff0, List 23; migrations 00659–00665 on the hosted QA project; simulate-activity redeployed.

  • ROUND 4 — one clock, empty profiles, rows (2026-09-23, plan W1 B1/B2, W2 F5/F6/F7/I3) · ONE-CLOCK built: every time of day is 24-hour "HH:MM" through the existing formatHm (deadlines, the late-arrival chip, the wizard's deadline receipt); the 12-hour helpers and the 오전/오후 keys are deleted; no-inline-date-format now also catches getHours()-style values fed to padStart — run over the whole codebase it found three more hand-rolled date builders, all moved onto toLocalDateStr · EMPTY-ONCE on profiles built: 경기 기록 / 매너 태그 / 업적 / 클럽 render nothing when empty (call-site gates sharing the component's own predicate); the public stat strip hides at 0 matches; a brand-new player's public profile reads one line; the own profile keeps its 친구 찾기 prompt as the one call to action · F5 records' footer links are ListRows (LinkCard deleted) · F6 venue rows use icon actions with RemoveIconButton's own confirm (also fixing a web double-fire), and the 테니스장 관리 list's first row gets its top inset (the container's missing padding, not a row rule) · I3 the 운영비 receipt renders its facts with FactRow.

  • ROUND 4 — PERSON-ROW (2026-09-23, plan W2 F1/E2/E3) · built: one PersonRow on ListRow (face then initials, name on two lines, one sub, exception badge, one action; default tap opens the profile with prefetch); the session roster preview, the three participant-row variants, pickup applicants, friends / add friend / blocked, DM participants, club members, head-to-head and the partner suggester moved onto it · the leaderboard, podiums, club ranking, standings and head-to-head now show the player's PHOTO — the RPCs already returned avatar_url; the client dropped it · the partner suggester shows the candidate's face, not a sparkle · no-hand-built-person-row (error, canary test) fails a new avatar-plus-name composed by hand · review correction: the lane parked 13 older hand-built rows in eslint.ratchet.config.mjs, which the owner's policy keeps EMPTY ("exemptions must be earned") — a follow-up lane migrates them (lists onto PersonRow, bylines and scoreboard slots onto PlayerIdentityChip) and returns the ratchet to []; the real PersonRow gets its own test (feature tests use the kit's twin).

  • ROUND 4 — KIND-GROUPS + a metric fix (2026-09-23) · KIND-GROUPS built: the 알림 할 일 band keys admin work by kind; a kind in one club is that club's row (tap → the kind's screen), a kind in several clubs is one row with "N개 클럽" that expands inline; neutral rows with a warning dot, no warning cards, no repeated 관리 tag; every filter chip counts the rows it reveals (전체 now includes the admin rows) · 00666 (my review of round 3's 00659): the since-join 출석률 counted every attended record in the numerator but only post-join sessions in the denominator — a guest-turned-member or a rejoined member could read above 100%; both sides now use the same window; pgTAP proves it (a guest-era attendance row no longer counts; attended never exceeds held); pushed to the hosted project.

  • ROUND 4 — settings and forms (2026-09-23, plan W4 H4/H5) · 설정 is five titled sections (계정 / 알림 / 프라이버시 / 앱 / 지원) instead of one 21-row list · 없는 장소 추가 and 제안 내역 left settings for the 테니스장 directory (a footer action and a header action) — the 경기-tab's inline finder still lacks them (queued with the venue-finder residuals) · the club's new-member mission is hidden from its owner and admins · the venue-evidence date uses the native date picker (one shared field for contributions and owner claims) · region is one dropdown instead of 17 chips · 회원 탈퇴's placeholder is a hint, not the confirmation phrase itself · the owner-claim venue search is VenueFinder (the one venue search), replacing a hand-built list that showed duplicates.

  • ROUND 4 — EXCEPTION-LEDGER (2026-09-23, plan W1 C2/C7) · built: 회비 관리 states 1인당 amount and the due day once (from the selected month's own rows), sections rows 확인 대기 / 미납 / 완납 / 면제 (empty sections omitted), and gives an unpaid row ONE 납부 확인 pill; 면제 / 메모 / 납부 취소 moved behind the row tap into one actions panel; dues rows render through PersonRow (out of the lint ratchet) · 출석 확인 rows are silent by default — one ✕ toggles 불참, and only a marked row carries a 불참 badge.

  • ROUND 4 — reports and dead ends (2026-09-23, plan W4 H2/H3/GM-15) · GM-15 the public profile's 신고 opens the real report flow targeting the user (the one ReportModal, moved into @twomore/app; the server already accepted user-only reports) · H3 the admin strip's 신고 N and the 알림 report kind now open a 신고 review screen (reported member via PersonRow, reporter and date, reason, resolve) — the repository, RLS and grants already existed, only the UI was missing · H3 a tournament with no rounds offers 라운드 만들기 instead of a dead "순위 미정"; there is no rebuild RPC, so it resets the empty tournament behind a confirm whose action reads 처음부터 (the consequence), not the goal · H2 공유 is its own header icon on the public profile; ⋯ keeps 신고 / 차단.

  • ROUND-3 REVIEW + ratchet (2026-09-23, adversarial reviewer on b3b114b3…b685d0c1) · P1 a live PICKUP never reached 오늘's live stack — the live list came from a club-scoped query, so an in-progress pickup sat under 예정: the live list is now the union of the club query and the viewer's own RSVP'd sessions (test: a pickup known only through RSVPs gets the live stack) · P1 대항전 counted and grouped challenges by their stored status, so one past its end date sat under 진행 중 while its own badge read 종료: stats and sections now use the same derived status as the badge (tests with a stale in-progress fixture; the existing fixture's "active" challenge had itself ended in August) · P2 the completeness rule counted archived media that the SQL excludes, and the parity gate could not see that clause: the rule requires archivedAt == null, the gate now pins all three media conditions on both sides (proved by a canary: dropping the TS clause fails the gate) and its dead sqlKeysPresent set is gone · the reviewer also verified the since-join SQL auth/ACL, the DM trigger and race fix, the dropped RPCs, every 클럽 ELO surface, the ELO-parity and edge-auth gates (bad inputs fail) · ratchet empty again: the person-row cleanup moved 12 more files onto PersonRow / PlayerIdentityChip (and gave PersonRow its own 10-case test); the one remaining entry was a CLUB logo beside its name, which now renders through ClubAvatar (the one club-avatar rule) — eslint.ratchet.config.mjs is []. Visual deltas from that cleanup (bylines below the name line, some tinted sub lines now neutral) are listed for device QA.

  • ROUND-4 REVIEW (2026-09-24, two adversarial reviewers on b685d0c1…208747b8, before round 4 shipped) · P0 REPORT-ONE-PIPE (00667): the new 신고 review screen, the admin snapshot's 신고 count and the trust tier all read member_reports, but every report a user files goes to content_reports — the club queue had no way in, while any signed-in user could INSERT club reports directly for any club and target, unthrottled, and the tier counted rows (three rows from one account demoted a trusted player). Now the one report form's RPC also files the club queue for a report from a club's or a club session's chat when both people are active members (a message report must name its sender); direct writes are revoked; resolve_member_report is the one resolve path (owner/admin, resolver = caller); the tier counts DISTINCT reporters and recalculates on filing and resolving; pgTAP 25 assertions; hosted verified by probe; rows show the form's own category label and the reporter's words · P1 dismissing one admin-work row hid every kind for its club — dismissal is per (club, kind) now (store v2) · P1 the 신고 route gets its web page · P2 guest rows' exceeded-limit warning is PersonRow's exception badge (the migration had dropped the tint) · P2 no-hand-built-person-row was blind to a ternary around the avatar and a name two stacks deep: widened (canary-proved), scoped to ROWS, and the two rows it then found moved onto ListRow (DM thread row: messenger right column, time above the unread dot) and PlayerIdentityCard (pickup host card, via one new footer slot) · gates: the blueprint gate's "today" was UTC while git history reads local time (a pre-commit run between 00:00 and 09:00 KST accepted dates the pre-push run rejected) — local date now; docs:lint-stale checks the README's "N repository migrations" (drifted 623 vs 658) · candidate for the owner (not built): a canonical person HERO — the public profile, edit profile, applicant detail and the DM intro each compose avatar + name by hand; heroes are out of the row rule's scope, and consolidating them is a wireframe-first decision.

  • ROUND 4 SHIPPED (2026-09-24) · commits 5578e186…87a54a98 (round 4 + its review), OTA 0ce3160e, List 24 (30 rows); migrations 00666–00667 on the hosted QA project.

  • OPEN-ISSUES PASS (2026-09-24, owner: "research and plan for evidence-based fixes on all the open issues, and then implement" — plan docs/design/open-issues-plan-2026-09-24.md) · inventory verified against code (32 open, 11 doc claims already stale), the page QA board (4 unresolved), and an Android emulator pass on OTA 0ce3160e (the Pixel_7 image is held by a zombie QEMU process until Windows restarts; the second AVD was used) · DEVICE REGRESSIONS (round 4, all tests had been green): 멤버 관리 showed no names (a PersonRow inside an XStack padding wrapper collapsed its name column — the wrapper is gone, the list rails itself, the hero bleeds by a named margin) · 회비 rows touched both screen edges (the list used the unrailed preset — now railedList/railedBanded) · empty grey avatars while a photo loads (AvatarBubble draws initials until THIS url reports loaded, so transparent logos never show initials through them) · DM inbox titles on three different x (a group avatar now keeps one avatar's footprint, two faces) · eleven filled 납부 확인 pills (row actions are secondary) · seeded DM threads opened empty (the sender trigger rejected service-role inserts and the seed only warned — 00668 + the seed throws) · RSVP-DEADLINE decided: the deadline is what the server already makes it (answer-by + the 00481 guest boundary); no surface calls a passed deadline 마감 — 응답 기한, the fact row omitted once passed, the reminder asks for an answer; a real hard close stays a future feature · REPORT-REVIEW: platform reports get an ops path (yarn ops:reports, two dev-account RPCs) — no platform-admin role, no client read access · NULL-SAFE GUARDS: auth.uid() <> x does not refuse a NULL uid; every dev-account guard is IS DISTINCT FROM now and pgTAP pins that no anon-executable function uses the unsafe form (canary: one stray anon grant is caught) · PERSON-HERO closed, not built: four of six sites already compose TypeHero; the DM intro and edit-profile avatar are different patterns · VENUE PROVENANCE: the last three free-text venue paths (club venues, the create-session wizard, edit-session's location) go through the one venue pick; not-found routes to 없는 장소 추가 · smaller: 경기-tab 테니스장 gains 없는 장소 추가 / 제안 내역 (one explore-only VenueFinder row), 경기 방식 is one fact row with its ELO consequence as a second line, per-field edit modals say 적용 (the page says 저장), the typed › became the chevron icon (lint no-typed-chevron), partner-suggester values no longer wrap, VenueCard's dead map zone deleted · prevention: yarn qa:layout — the web rung fails on off-rail or collapsed text (jest can't measure layout; both P1s are its canaries) · CLUB-ELO-POOL: evidence and a recommendation (retire the pool; ranks from the global rating) are with the owner — it reverses part of the ratified 조 model, so nothing was built · GitHub #10 closed as superseded by the device lists and the board.

  • OPEN-ISSUES SHIPPED (2026-09-24) · commits 1370f797…7976964f, preview OTA d1d5eb27, List 25; migrations 00668–00669 on the hosted QA project; send-push and seed-scenario redeployed; every device finding re-verified on the emulator; three board items closed with evidence; GitHub #10 closed.

  • CLUB-RATING-UNIFIED (2026-09-24, owner approved the CLUB-ELO-POOL recommendation: "Yeap, I agree with your recommendation"; plan docs/design/club-rating-unification-plan-2026-09-24.md) · a player has ONE rating per discipline (player_ratings); a club's ranking is that rating among its active members, never a second pool. rating-system-v2.md §7 always specified this — the per-club pool had drifted in beside it, gave every player two disagreeing numbers, and get_session_rating_impact ranked one against the other · migration 00670: public.get_club_rating_ranks (INVOKER; competition rank among members with a rated match, unrated members unranked, the order defined once in the RPC), private.club_player_match_counts keeps the one per-club signal (home-club calibration, §11.4), analytics' 평균 ELO is the members' global average, the session rank lens compares on one scale, club_elo_ratings dropped · client: ClubMemberRating + useClubRatingRanks; every caption says ELO (ratingScopeLabel, the 클럽 ELO key and the dead clubEloEnabled entitlement retired); rankings list ranked members only, ranks from the server (ties share a number) · canon clubs.md ONE-RATING supersedes CLUB-ELO-LABEL.

  • POST-COUNTERS (2026-09-24, found while fixing a seed defect) · the 소식 comment/reaction/read counts only moved when the author or an admin engaged: the count triggers were SECURITY INVOKER, so a member's UPDATE club_posts hit the authors-and-admins UPDATE policy and changed 0 rows (probed: 2/6/0 before and after), and an optimistic reaction snapped back on refetch; seeds hid it by hand-setting counts (club_showcase read 2x). An author could also PATCH their own post's counts · migration 00671: definer counters, counts start at 0 on insert and no client writer sets them, every post backfilled · class gate trigger_cross_table_writes_definer.test.sql (an invoker trigger — or an invoker helper it calls — that writes another RLS table fails; two canaries; the one other hit was moved to definer so the gate holds with no exemption) · canon data-and-hooks.md.

  • SMALLER (2026-09-24) · club initials skip brackets and symbols ([참가자뷰] 라이브 read [라; letters and digits only, Hermes property escapes verified with hermesc) · two pgTAP assertions that counted global rows (court reviews, venue observations/provenance) failed on any seeded local stack — scoped to their fixtures; 28 other whole-table counts pass because RLS or in-transaction setup scopes them (follow-up: a static rule) · the product-management plugin (synced from claude.ai) disabled in user settings at the owner's request.

  • CLUB-RATING SHIPPED (2026-09-24) · commits 5457c904…12047f00, preview OTA 930dbd7c (after e8d36f70), List 26; migrations 00670–00671 on the hosted QA project (probed: ordered ranks with real ties, club_elo_ratings gone, post counts equal their rows); seed-scenario and simulate-activity redeployed · emulator: club 순위 shows server ranks with ties (1, 2, 3, 4, 4, 6 …) and the ELO caption, the records club tab and podium read ELO, initials skip brackets · the device pass caught a regression the tests could not: the avatar stack's count chip went through initials derivation and lost its "+" — AvatarBubble takes a verbatim label now (the second OTA).

  • OPEN-ISSUES 2 (2026-09-24, owner: "Let's work on the open issues first") · inventory: no open GitHub issues; the QA board's one open item is U-01 sign-up (Phase 8, external); every EXT row needs the owner or a third party · GPS check-in, retired by 00384 in July, still lived on: the privacy policy names it in four places (legal text changes only after counsel approves, per the repo's own process — the exact passages, proposed wording, a verified location inventory (the app reads location twice, on the device only; nothing leaves it; uploaded photos lose their EXIF location on the device) and two counsel questions (is the 위치기반서비스 신고 still required; is the location consent still mandatory) are in pre-launch item 2, EXT-2/EXT-3 updated), the dev panel still seeded a scenario for its missing button (retired end to end), two code comments and a canon example cited it · HERMETIC PGTAP: 11 whole-table signal wipes and 17 whole-table counts now read only their fixture (created_at >= now() is the test's own transaction); 00031's "time-of-day" failure was the same class (a picker's LIMIT 100 filled with 2,135 older seeded signals); gate check:pgtap-hermetic (property-based exemptions, 17 canaries); the full suite passes on a seeded local stack for the first time; concurrent runs no longer deadlock on wipes (fixture ids are fixed by design: one suite at a time) · DOC LINKS: VitePress ignores dead links, and eight active-doc links pointed at moved or retired files (the checklist sent counsel to the deleted GPS hook) — gate docs:lint-links, one shared active-docs list for both doc lints · UNWIRED TESTS: three script test files (the layout audit's canaries among them) were in no package.json script, so they never ran — wired, and gate check:script-tests-wired · canon data-and-hooks.md: the hermetic-test rule.

  • OPEN-ISSUES 2 SHIPPED (2026-09-24) · commits 227413c3…5955a369, preview OTA 4c43f62d (dev panel only — no user-visible change, no device list); seed-scenario redeployed; the full pgTAP suite passes on a seeded stack (148 files, 3,321 tests).

  • OWNER QUESTIONS DECIDED (2026-09-24, owner: "What do you think are the evidence-based and intuitively appealing solutions to the problems you are posing? Do your research on the apps we model after and make the changes") · CLUB-IDENTITY-BAND (D3): 87% of clubs (99/114 locally) showed a stock court photo hashed from ten images — repeated across clubs and not the club's court. Kakao gives a photo-less profile its own colour, never a repeated photo; Spond colours groups; NN/g: decorative images displace content; identical images merge perceptually (repetition blindness); Meetup: organizers who replace the generic default see join-rate lifts. So a photo-less club renders a short band in its OWN flag colour (the corners law unchanged; real photos keep the 3:1 window), the stock-photo fallback is retired everywhere, and a club's media managers get one "사진 추가" on their photo-less banner (the real lever). EXT-16 no longer needs club default photos · FRIENDS-GOING: friend-based social proof is the strongest kind (Facebook Events, Meetup, Luma show who's going), but the card canon keeps faces off SessionCardV2 and that card has been reversed twice, so the signal enters through the strip's designed extension point: a friends_going chip for a browsing viewer while a session is still open ("친구 참석 | 2명") on the session cards, fed by one list-scope friends read (Protocol A); the detail hero carries no strip since the page-native ruling (2026-09-20), so on the detail friends lead the roster with a 친구 badge (precedence 나 > 호스트 > 친구 > 예비) — verified on the web rung. Open (a privacy decision, not built): 번개 찾기 lists pickups whose roster a non-member cannot read (RLS), so the chip cannot show there; showing a stranger's pickup to their friends would need a count-only RPC and a privacy ruling · PICKUP-RULES: the question's premise was false — a pickup host owns their pickup club and can already edit match rules on open/locked sessions. The real gaps (Meetup and TeamSnap notify on edits; Playtomic locks friendly/competitive at play) are closed: a match-rules or friendly/competitive change sends session_updated to confirmed and waitlisted players, the server refuses a rules change on a finished session, the match-rules screen says players will be notified, and two comments that claimed the host couldn't edit are corrected.

  • OWNER QUESTIONS SHIPPED (2026-09-24) · commits 96ccc0e8…ecc6fee3, preview OTA 6631a5d0, List 27; 00672 on the hosted QA project · two adversarial passes: every replaced function matched its live body, a stranger's probe saw no pickup roster; fixed from review and from the web rung: 저장 and its notify notice now wait for a real change (matchRulesEqual), the club top bar's glyphs read over a light band (expandedInk), a real photo loads over its club colour, and web captures wait for the network to go quiet (a friends query had been captured unsettled) · on the hosted QA world most clubs carry seeded album art, so the band shows on the photo-less ones (seen in 탐색).

  • REALISTIC-SEED-MEDIA (2026-09-25, owner: "replace photos and logos with realistic examples so that we can really test out the visuals") · the seed world's club photos were procedurally drawn PNGs (flat colour fields with dots) and two club logos were random face photos (i.pravatar.cc), so no seeded surface showed what real content does to a card, banner, album or avatar · PHOTOS: 39 photos taken at real courts and clubs: Korean public courts from Wikimedia Commons (수원 만석공원 — green windscreens, shade tents, apartment blocks behind; 동암고; 보성 clay under mountains), night play under floodlights, doubles, handshakes, group shots, courtside drinks, lessons, gear, from Unsplash; plus one deliberately blurred shot for the moderation reject fixture. Licences: Unsplash License and CC BY / CC BY-SA, credited per photo in the catalog · LOGOS: 218 original emblems (no real club's mark) in the ranges clubs actually use: roundel seals with ring lettering, shields with ribbons, serif monograms, bold Hangul wordmarks (Black Han Sans), Hangul seals, script, sporty badges, ball-and-racket marks, plus four photo-as-logo crops. A club wears the place it is named after — every district and region the generators use (REGIONS, 73 places, Revised Romanization lettering, three variants per district so neighbours rarely match), matched at the start of a word with the district before its region (강남구 never reads as 남구; 부산진구 wears 부산진) — and a club named after no place wears a brand (에이스, 랠리, 위너스 …) or a photo. The first cut matched a hand list of 18 districts, so the 탐색 list showed the same 랠리 badge on neighbouring clubs; caught on the web rung and replaced before anyone saw it · SHARE: half the generated clubs carry an uploaded logo, hashed per club id; the rest keep the generated flag mark, so both states stay testable; fixtures keep their intent (club_cards M1/D1 photo + logo, M2/M3/D2 none; club_growth bare none) · SPEC: every fixture is built to the app's own upload output (IMAGE_RESIZE_PRESETS: gallery 1600, banner 1280 16:9, logo 512 square; JPEG at IMAGE_RESIZE_DEFAULT_QUALITY; EXIF stripped) and copied server-side into each club's own buckets and paths, so seeded media behaves like uploaded media · PIPELINE: media-catalog.ts (one catalog), scripts/seed-media.mjs --local|--linked (build + upload to the private seed-fixtures bucket through the CLI, no service key on the machine), lib/seed-media.ts (copies), seed_media_refresh (re-dresses an existing world without truncating: drawn PNGs, face-photo logos, retired-bucket banners, and logos the seed itself put there — recognised as byte copies of a fixture by ETag, which a server-side copy keeps; a person's own upload never matches and is kept; a second run changes nothing). A fixture's content never changes under its key (SEED_LOGO_SERIES moves on a redesign), and seeded logo/banner URLs carry ?v=<fixture> so a phone never shows a cached old image. Seeded public URLs are written on the URL the caller used (run-scenario.mjs sends it): inside the local functions runtime SUPABASE_URL is the internal gateway, so locally seeded logos and post photos never loaded in a browser (the post photos never had). qa:seed --local builds and uploads the fixtures itself · RETIRED: the procedural PNG generator (club_growth/images.ts), the stale fetch-default-club-photos.mjs, and — migration 00673 — the retired default-photo bucket's any-signed-in-user write policies, an open upload slot on a public URL once its only writer was gone (bucket removal via the Storage API is on the pre-launch checklist with seed-fixtures) · FOUND IN PASSING: qa_club_ops proposed a rated guest to a club whose random tier band could exclude them (the RSVP gate refused it on about one run in five) → the proposed guest is now an unrated newcomer, which the gate admits by design (00282); one type error in qa_club_contents.ts (overrideTypes for a runtime column list; the seed function has no type-check gate — npx deno check --no-lock supabase/functions/seed-scenario/index.ts is clean now); full_qa failed three times on the local stack with 502 "An invalid response was received from the upstream server", each time on a write sent right after a HEAD count — the gateway reused a pooled connection the API had just closed; the drawn-PNG seeds hid the race by being slow, fast server-side copies exposed it → the seeds' 8 HEAD counts became GET counts and the idempotent media writes retry that error; a club's logo URL never changes when an admin replaces the logo (stable path, no version), so a phone can keep the old one from cache — noted, not changed.

  • COVER-ALWAYS (2026-09-26, owner: "On club discovery card list, we should always render the banner photos. If there is no photo uploaded, we give them default options") · owner veto of CLUB-IDENTITY-BAND (D3, 2026-09-24). Every club surface that leads with a photo renders one, through one rule (resolveClubCoverUrl): album photo → banner → the club's assigned default cover. The rule covers the card, the club-detail banner, the public-profile pager and the settings preview.

    • The set: 27 court scenes (hard, clay, grass, indoor, night; no people), up from the retired 10, so two neighbouring cards repeat a photo about 1 time in 27 instead of 1 in 10. Unsplash/Pexels licences need no credit.
    • Choice: 클럽 설정 gains 기본 사진 선택, a gallery beside the upload button (the Notion cover gallery / Band default-cover shape).
    • Why defaults and not the band: the owner reads an empty colour field as a missing photo on a list whose job is choosing a club.
    • Rejected: keeping the band on the card only (two grammars for one slot); generated artwork (licensed photographs already exist, and generated scenes read as fake); people in defaults (they would pass for the club's members).
    • Canon: clubs.md "Photo, 3:1 window — COVER-ALWAYS". Two reversals on this slot, so the law records its history.
  • CARD-FACT-GROUP (2026-09-26, owner: "The spacing between the two meta data rows on session summary cards is too wide") · SessionCardV2's L2 (⏰ time · 🎾 format) and L3 (🎯 tier band) are one fact group at SEAM.element (4px), with SEAM.item (8px) from the title. At 8 apiece the three lines read as three separate items; the seam ladder puts parts of one item a rung closer than its neighbours. Rejected: a new 6px value off the ladder. Canon: sessions.md.

  • FLAGGED ISSUES, TREATED (2026-09-26, owner: "As for issues you've flagged, give them evidence-based treatment") ·

    • (a) Stale club logo. Evidence: useUploadClubMedia overwrote <clubId>/logo in place. expo-image caches by URL and the Supabase CDN caches public objects (1 h default), so a replaced logo kept showing. Fix: a fresh path per upload (<clubId>/<kind>-<timestamp>), then delete the replaced object. This is the pattern useUploadAvatar already used; 3 tests.
    • (b) dense_compute found nothing to score (the ~3 am flake). Root cause: the seed lost its own data, in six layers.
      1. Sessions were inserted already finished, and assign_waitlist_position (00406) refuses a confirmed RSVP on a finished session: 209 RSVPs lost per run. Sessions now insert as 'open' and move to their status after RSVPs, payments and matches exist, which is the product's own order.
      2. Settle-before-play (00384) refused whole match batches because seated players hadn't paid. A finished fee session is now settled: every confirmed attendee has paid, and a fee-exempt host is waived. This also stops expire_unpaid_session_holds cancelling hosts' and unseated attendees' RSVPs an hour after seeding.
      3. A club's tier band was random and unrelated to its members. 29% of matches seated a player the RSVP gate refused, and the real user (silver) sat in gold–grandmaster clubs. Now the band centres on the owner's tier and members and attendees come from the players it admits. The seed's copy of the tier floors is held to tier.config.ts by a parity test.
      4. Six clubs completing sessions at once deadlocked on shared profiles: 226 deadlocks and a 150 s timeout. Status writes now queue behind one writer.
      5. Two reads stopped silently at PostgREST's 1,000-row cap. Memberships lost the last clubs, and dense_compute saw 1,000 of ~2,000 matches. Both now page (selectAllRows).
      6. Scoring every match then outran the edge runtime's CPU limit at 3 HTTP calls per match. Migration 00674 adds seed_score_matches (service_role only, serial in the DB, pgTAP 7/7), and it takes 40 matches a call. Results across the clean local dense runs: 0 matches seat a player without an RSVP (was 465 of 1,587), 0 finished sessions keep a pending hold, 0 deadlocks, and every scored match is rated (1,608–1,860 per run, 0 scoring errors). dense_matches takes 27–33 s, down from 52 s.
    • (c) Identity-band colour clashing with an uploaded logo. Dissolved by COVER-ALWAYS: the band is gone, and its colour only grounds a cover while it loads.
    • (d) White-background logos in dark mode. An uploaded logo now wears a $borderSubtle hairline, shown exactly as uploaded otherwise. Slack, Discord and KakaoTalk show uploaded icons as-is; GitHub Primer rings avatars. Rejected: tinting or inverting the logo (it alters a club's own mark).
  • SEED-ROSTER-FIRST (2026-09-26, found verifying (b)) · The same RSVP-on-a-finished-session loss ran through 13 scenario layers, not only the generator. live_showcase's live sessions had lost their rosters, and its fee sessions' matches were refused by settle-before-play. One helper (lib/session-status.ts: insert 'open', write the roster, then settle the status) now serves them all, with fees recorded before RSVPs. live_showcase keeps its 결제 대기 / 송금 완료 demo payers off court, and everyone else has paid. Rejected: per-file ad hoc reorders (three files already had their own, which the helper replaced).

  • WEB-COACHMARK (2026-09-26, found on the web rung) · Home on web showed the error boundary as soon as a live session started the live-pulse coachmark. react-native-spotlight-tour's overlay passes a style array to a DOM <rect> through react-native-web, and React DOM throws. Coachmarks are native-only: TourRefInjector leaves the tour unset on web, which covers all three tours (home, live stack, session detail), with a test. It surfaced only once the seed seated the dev user in a live session. Rejected: patching the library (a web-only onboarding aid, not worth a vendored fork).

  • SEED RESIDUAL (2026-09-26, open, low) · On a clean local qa:seed, the finished-session refusals are gone. What remains is about 10 match refusals per run, all on the dev account: round uniqueness (seated twice in one round) and settle-before-play (seated unpaid in a fee session). They come from a scenario layer, not createSessions. insertDoublesMatchForUser now logs its refusals instead of dropping them, so the next run names the layer. Everything else in the local DB log is unrelated to the seed's data (the local cron's missing push URL, stats upserts racing).

  • COVER-ALWAYS SHIPPED (2026-09-26) · commits f05d2c4a…7e30583f, preview OTA d9673491, List 29 · 00674 on the hosted QA project, seed-scenario redeployed, all 27 default covers on hosted storage · local verification: yarn check green, qa:seed --local 29/29 steps, web rung (discovery list, club page, public profile, settings; home after the coachmark fix).

  • PHOTO-BAND-2:1 (2026-09-27, owner: "the size of the photo might be too small. Can you do some research on the best evidence-based proportion and size?") · The club photo at the top of the club home and its public profile is 2:1 of the screen width: 195pt on a 390pt phone, up from 132 (about 3:1), capped at 280pt on wide windows. It is one rule (resolveHeroBandHeight), and both collapse points derive from it.

    • Evidence: group pages cluster at 2:1. 당근 모임 covers are 2:1 (132px had claimed "당근 scale"), Strava clubs about 2.04:1, Facebook Groups 1.91:1, Meetup 16:9, Band 4:3. Our uploaded banners and default covers are 16:9, so 2:1 crops them only slightly.
    • Rejected: 16:9 or taller, Airbnb's 3:2 gallery job. It would push the club's name and tabs toward a false floor (NN/g). Static-map heroes (session, venue) keep 132px: a map is context, not the subject.
  • PHOTO-SCRIM (2026-09-27, owner: "the navigation and menu icons and the 앨범 overlay that all render on top of photos aren't shown correctly as the photos can overpower them") · Controls over a photo sit on a black edge gradient (PhotoScrim).

    • Top: 50% at the top and 45% through the bar, fading out 40pt below it; it disappears when the bar goes solid.
    • Bottom: 45%→0 over 56pt, under the dots and the album pill.
    • Why 45%: it is the lowest opacity at which white on a pure-white photo still meets WCAG 1.4.11's 3:1, and a test checks the arithmetic. It matches Material's scrim and Airbnb/Google Maps photo headers.
    • Also fixed: the public profile's N/M pill moved from under the ⋯ to bottom-right.
    • Rejected: per-glyph circles (owner ruled them noise on 2026-08-23); raising the modal backdrop token (it would darken every modal).
  • FEED-TWO-LINES (2026-09-27, owner: "I don't think we need to render the name, date and category into three rows. Perhaps two rows is enough? … icons, embedded links, avatars, texts and all the other meta data") ·

    • Header: every feed row's header is two lines, name then one meta line (time and board, or time and 자동) as MetaSegments. That is the Facebook / Band / Threads post header, still without the banned middle dot.
    • Avatars: 40pt (Discord and Facebook 40, Threads 36).
    • One shape: member posts and system rows now share one ListRow header. System titles had been weight 800 with tertiary micro meta.
    • Session link chip: date and format share one line at meta size instead of two stacked lines of badge text.
    • Links: post bodies render through AutoLinkText, the canon's one body renderer, so pasted links are tappable in the feed as on the post page.
    • Rejected: name and time on line 1 with the category as a chip on line 2 (a chip on every row adds noise); link-preview cards (no URL-metadata fetch exists yet, so it would be a server feature). Feed photos stay 72×56 thumbnails, an open question for the owner.
  • SESSION-ONE-FACT-LIST (2026-09-27, owner: "the separation of the information on the hero at the top and the 정보 at the bottom [is] a bit forced. Is there a natural way to combine the two in a seamless fashion?") · The session page states every fact about the session in ONE list under its title, in the order a player decides in: 일시, 종목, 경기 유형, 경기 방식, 실력, 참가비, 취소 규정, 응답 기한, 날씨, 로테이션. The 정보 section below the roster and matches is gone. 종목 now shows the format alone, so the event word appears once (경기 유형), and the ELO line gained its subject ("ELO 반영", it had read a bare "반영" since the rows merged). The public pickup preview's list gains the same rows.

    • Evidence: logistics-first event pages keep all of an event's facts in one block near the top (Luma's and Meetup's details panel, Airbnb's key-facts area). Splitting one kind of information around unrelated sections makes the reader hunt.
    • Rejected: a collapsed "더보기" for the conditions (hiding short facts costs a tap for no space saved); dropping 경기 유형 (an earlier decision, D3/D5).
  • COUNT-BESIDE-TITLE (2026-09-27, owner: "is there a way to indicate the count of participants without rendering it as part of a section title? … render the meta data on the right of the title row and use the 외 n명 as the 전체 보기 redirection link") · The participant section is titled "참석", with "12/16명" (and "예비 N명") on the right of the title row as quiet meta segments. The "전체 보기" link is removed. The preview ends in one door, the "외 N명" row, led by the next three faces.

    • Evidence: a count sits beside its label in iOS/Material list headers. A preview list ends in one "show all N" (Airbnb amenities, Google Play reviews), and the header link opened the same page, so it was a second door to one room.
    • Rejected: the card's recruitment meter in the header (its status word assumes an open session and would read 모집 중 on a finished one).
  • 2026-09-27 SHIPPED · PHOTO-BAND-2:1, PHOTO-SCRIM, FEED-TWO-LINES: commits 20e5e6f3…25430b21, preview OTA 5f26c378. SESSION-ONE-FACT-LIST, COUNT-BESIDE-TITLE: c4877efd…be6062c0, preview OTA 80baf5ab (includes the first). List 30 · yarn check green; web rung checked the club page (light + dark), public profile, venue page and session page.

  • SESSION-FACTS-COMPACT (2026-09-27, owner: "Do you think there's a way we can consolidate and compact the information in the information block?") · The session fact list is one row per question a player asks: 일시, 종목, 실력, 참가비, plus 내 상태, 취소 규정 and 날씨 when they apply. Short facts of the same answer share the main line as segments ("복식 │ 정기 │ 경쟁"), and supporting facts sit on the quieter second line ("응답 기한 …" under 일시; "ELO 반영 │ 30분마다 교대" under 종목). The result is at most 7 rows instead of 11, and 5–6 on most sessions. An unset deadline gets no "없음" row. The cancel rule reads "시작 12시간 전부터 늦은 취소" instead of a bare "12시간 전".

    • Evidence: chunking related facts (Miller, NN/g); Luma's and Google Maps' fact rows (a primary line and a quieter second line).
    • Rejected: label-less icon rows (they reverse the 09-20 labelled-rows ruling, and labels help first-time users); a two-column tile grid (worse for scanning mixed-length values).
    • Rule change: FactRow's second line now holds the row's supporting facts (canon components.md), superseding the 09-24 consequence-only rule.
  • FEED-PHOTOS (2026-09-27, owner agreed: "sure, I agree with your assessment on the feed") · Feed photos are big enough to see. One photo spans the row at 4:3; two are side-by-side squares; three or more are three squares with "+N". This follows the Facebook / Band collage pattern, kept to one line so a row stays scannable. The same layout serves member posts and photo-upload rows.

  • 2026-09-27 SHIPPED (later) · SESSION-FACTS-COMPACT, FEED-PHOTOS: commits 93c47cf8…42413352, preview OTA d30a5f27, List 31 · yarn check green; web rung checked the session page and a photo-heavy club feed.

  • SESSION-EDIT-IN-PLACE (2026-09-27, owner: "the fact block becomes the place to edit all information … let's just ensure we design and implement the editor in the canonical way") · The session page is where its host edits it. 수정 in the header turns the fact block into one row per field and flips to 완료. Editable rows trail 수정 and open that field's panel, which applies into a draft. Fixed fields show a lock and say why. 완료 saves only what changed, in one update. It asks first, naming how many attendees are told, exactly when the save alerts them: date, time, place or fee, the server trigger's own columns. Leaving with unsaved changes asks to discard. The separate 일정 수정 page and its host-tools row are deleted.

    • Evidence: Apple Contacts/Calendar Edit → Done, with Done in the nav bar (HIG). Google Calendar and Luma save a whole record once. Per-field autosave on a record others depend on is not an attested pattern. NN/g: a mode needs at least two visible signs (the header word and the hint line), and a confirmation belongs only on consequential actions. A lock with no stated reason confuses users.
    • Rejected: keeping a separate edit page (the page already shows every field, so a second screen restated them); saving per field (each change would alert attendees separately).
  • REVIEW-IS-THE-RECORD (2026-09-27, owner: "merge the edit pages so that the shared edit page is deployed and is updated with best practices") · The create wizard's 확인 step and the page's edit state render ONE session record (session-record/). Both use the same fields, order, labels, glyphs and value formats; the only difference is where a tap goes. On the review, each row goes back to the step that asks it and returns (the armed detour). On the page, each row opens the field's editor. A consequence sits on its own row at the point of decision: "만든 뒤에는 바꿀 수 없어요" on 종목, 한 경기 시간 and 응답 기한, and the auto-cancel minimum on 최대 인원. This replaces the review's card and its one generic notice. The club wizard's review uses the same row grammar, and WizardReviewRow is retired.

    • Evidence: GOV.UK uses one summary-list component for a form's check-answers page and for a saved record, and its "Change" link returns the user to the question and then back. Shopify Polaris uses one resource-details layout to create, view and edit. GOV.UK's task list found users tap anywhere on a row, so the whole row is the target, named for its field ("장소, 수정") for screen readers.
    • Rejected: opening the page's field panels from the review (the wizard's steps are the richer editors: suggestions and dependent fields); a separate summary card per wizard (two grammars for one pattern).
    • Drift fixed on the way: (1) The review showed a fee typed and then switched off, which the submit never charges; wizardFeeAmount is now the one rule for both. (2) The page's level editor labelled "club default" as 무관; it now says 클럽 기준 그대로 (pickup: 실력 무관), as the wizard does. (3) The description field had two names, 한 줄 소개 in the wizard and 공지 on the page; it is now 공지 everywhere, with one limit (SESSION_DESCRIPTION_MAX 500, where the wizard allowed 200). (4) Rotation and match rules were missing from the edit state; the record now shows every field.
  • WEB-MODAL-STRICT (2026-09-27, found checking the edit state on the web rung) · Every react-native-web modal (ModalPanel, the venue search) was invisible in web development. Under React Strict Mode, RN-web's ModalPortal lost its container on the dev-only effect replay. Production was unaffected. A strict-safe copy is swapped in by apps/web/next.config.ts, which only touches the web build. I did not use a Yarn patch because the lockfile change would have tripped the OTA guard and forced an app build. scripts/screenshot-web.mjs --tap now sends trusted taps, since a DOM click() does not reach a press-tracking pressable.

  • 2026-09-27 SHIPPED (evening) · SESSION-EDIT-IN-PLACE, REVIEW-IS-THE-RECORD, WEB-MODAL-STRICT: commits 0f94b58f…e592c871, preview OTA 31371ae5, List 32 · yarn check green; web rung checked the page's edit state (every field row, locks with reasons), a real tap opening a field editor, and the wizard's review.

  • EDIT-MODE-NAV (2026-09-27, owner: "Make sure we take proper care of navigation and navigation components as well") · While the host edits a session, the bar offers only the mode's two ways out. 취소 replaces the back chevron in the leading slot, in regular weight. 완료 is the only control on the right; save and ⋯ hide. 취소 and Android back leave the mode, asking "수정을 그만둘까요?" only when something changed. The page narrows to the hero and the edit block: the map band's venue link goes inert, and alerts, the notice, 참석, 경기, 호스트 도구, 위험 구역 and the CTA band hide until 취소 or 완료. New primitive: DetailShell headerLeft (AppHeader/CollapsingTopBar left) and HeaderTextAction prominent={false}.

    • Evidence: Apple's Contacts, Calendar and Settings swap back → Cancel and show Done while editing, and Contacts hides its action rows in edit mode. M3's full-screen editor has a close control, never a back arrow. NN/g on modes: a poorly signalled mode causes errors, so the mode needs clear edges.
    • Rejected: keeping the back chevron as "leave the mode" (it promises leaving the page); leaving the rest of the page live (a tap on a roster row would navigate away mid-edit).
  • LEAVE-GUARD (2026-09-27, same ask) · Unsaved input is never lost to a stray back or swipe. useLeaveGuard locks the iOS swipe-back while changes are pending (gestureEnabled: false). It stops any other removal of the screen and hands it to a confirm, guards reload and tab close on web, and gives a release() for the screen's own post-save navigation. useUnsavedChangesGuard(dirty) pairs it with the app's one leave question (the wizards' exit copy: 나갈까요? · 계속 작성 / 삭제하고 나가기). It is used by the session edit mode (leaving the page), 경기 규칙, 프로필 수정 and 글쓰기, each of which lost typed input silently before.

    • Evidence: HIG: don't let a swipe discard input (isModalInPresentation, the interactive pop gesture); Android's predictive-back guidance intercepts only while there are unsaved changes. React Navigation's beforeRemove covers the header back, programmatic back and replace.
    • Rejected: usePreventRemove. Its cancelled native swipe desyncs JS and native and blocks touches on this exact stack (react-navigation#13072, open: native 7.2.2, RN 0.83, Expo 55; I verified the issue directly).
  • 2026-09-27 SHIPPED (night) · EDIT-MODE-NAV, LEAVE-GUARD: commits ccb2a9fa…05c77e09, preview OTA 3673febd, List 33 · yarn check green; web rung checked the edit mode's 취소/완료 bar and the narrowed page.

  • MAP-BAND-2:1 (2026-09-27, owner: "make map photo on top of the session detail page match the size of that on the club detail page") · Every place page's hero uses one band: the session map, the venue map, the directory venue map and the club photo are all 2:1 of the width (195pt on a 390pt phone, capped at 280). The map is requested in that shape at retina scale (640×320 at scale 2 = 1280×640 px, one venueHeroMapOptions), so the taller band is not a cropped, upscaled strip; I checked the size against the live static-map function. The collapsing bar flips where the band passes under it by default. The "photo hero" names became "hero band".

    • Evidence: the same 2:1 group-page norm as PHOTO-BAND-2:1 (당근 모임, Strava, Facebook Groups). Tapping from a session map to its venue page now keeps one band size.
    • Rejected: growing only the session map (the venue pages it links to would shrink back to 132).
  • FACT-ROW-RHYTHM (2026-09-27, owner: "is there a way we can make each information row visually more separate from other rows without adding clutter?") · Rows now read as separate through proximity, with no lines. A row's own lines stay flush. A fact list whose rows carry a second line or a note spaces every row 12 per side, so rows sit 24 apart instead of 16. A list of one-line rows keeps the tight 8. There is one rhythm per list and no hairlines.

    • Evidence: NN/g on proximity (the gap between groups must exceed the gap within one). Material's list spec keeps an item's lines on adjacent leading and pads three-line items 12dp. NN/g and Material both treat dividers as the fallback when spacing fails ("use sparingly… clutter").
    • Rejected: inset hairlines (clutter, and they reverse the 09-06 divider law for peers); a card or tint per row (boxes on a flat page); label-above-value stacking (roughly doubles each row's height).
  • 2026-09-27 SHIPPED (late night) · MAP-BAND-2:1, FACT-ROW-RHYTHM: commits 0dda0b88…229aea3c, preview OTA 1c8cc520, List 34 · yarn check green; web rung checked the session page's 2:1 band and the 24px fact-row rhythm; the 2:1 retina map size was checked against the live static-map function.

  • EDIT-SHEET (2026-09-28, owner: "the edit doesn't open a modal but render a bottom edit sheet") · Single-field editors rise as a bottom sheet: ModalPanel placement="bottom", full width and flush to the bottom edge, with the page visible above it. Every EditControlModal and EditFieldModal uses it (the session record's fields and the club settings list). Each ends in a full-width 취소 (secondary) · 적용/저장 (primary) pair. The scrim or 취소 closes it, and a sheet has no X. Confirms and pickers stay centred dialogs.

    • Evidence: Material 3 modal bottom sheets (an alternative to simple dialogs on mobile); Toss's bottom sheet with its fixed bottom CTA; NN/g (a bottom sheet needs a visible close, which is 취소 here).
    • Kept from STYLE-3: no drag handle, snap points or swipe. The drag-sheet libraries were retired for crashing the web bundle and the "panels don't open fully" bug, and a sheet that looks draggable but isn't frustrates. A grabber was rejected for the same reason.
  • EDIT-PENCIL (2026-09-28, owner: "instead of 수정, an edit icon implemented canonically") · An editable row trails a pencil (EditCue: lucide Pencil through Icon, md, $textTertiary, the same size and tone as the chevron and the lock) instead of the word 수정. It applies everywhere a field edits in place: the session record, both wizard reviews, and club settings. The row's accessible name stays "장소, 수정", and a pencil never sits beside a chevron. The session bar's own edit control is the pencil too: a HeaderIconButton for the host in place of the bookmark, since saving your own session to bookmarks earns nothing. That also brings the bar back to two glyphs with ⋯, where it had three.

  • EDIT-PAGE-BARE (2026-09-28, owner: "an edit page probably doesn't need a thumbnail photo and the meta text") · The session page in edit mode drops the hero and the hint line. The bar is a plain header titled 일정 수정 with 취소 · 완료, which gives the mode its two visible signs (NN/g). The body is the record rows alone.

  • MAP-BAR-INK (2026-09-28, owner: the controls on the map "would not be rendered properly against the background", as with the club photos) · The session map band gets the club photo's treatment: the top PhotoScrim and white glyphs. The page-specific dark-ink override is removed.

  • MAP-NO-CHIP (2026-09-28, owner: "the 장소 보기 chip probably can go away (as long as the map photo, when clicked, leads back to the location page)") · The pill is gone and the map still opens the venue page. A map band is its own cue (people expect to tap a map), matching the venue pages. A photo band that navigates keeps mediaHint.

    • Evidence against, noted: the research recommended keeping a small cue, because the tap destination is ambiguous; that finding was hedged and single-sourced. The owner's call stands, with no other tap target on the band to confuse it with.
  • ONE-BOTTOM-CTA (2026-09-28, owner: "get rid of extra control at the bottom of detail pages … We will have one main CTA at the bottom, and the rest should be inside the menu or top right") · The session page's 호스트 도구 and 위험 구역 sections are deleted. Their items moved to the header ⋯ menu, after share, group chat and 문의하기: 참석자 관리 · 경기 규칙 · 호스트 관리 · 출석 확인, then in red 경기 종료 · 일정 취소 · 일정 삭제, each behind its confirm. The tournament board's bottom 대회 취소 moved to a ⋯ beside its status. Settings pages keep their bottom danger zones, as the platform does.

    • Evidence: HIG menus put frequent actions first and destructive ones last in red; a detail page's single primary action goes in the pinned band (canon).
  • BADGE-DARK-FILL (2026-09-28, owner: rework the host badge's colour on the dark background "and examine if other badges have the same issue") · The audit found every dark badge fill too close to the page: neutral 1.04:1 on $card, muted 1.02, info and premium 1.06, and error, success, warning and accent between 1.24 and 1.38. The quiet surface badge sat at about 1.2 in three themes. All dark fills now clear 1.5:1 on $card and $background in every theme, and text on them stays ≥4.5:1 (muted, success and accent text were lightened a touch). surface uses a new mode-aware $badgeSurfaceBg. check:color-contrast now gates the fill against the page, and a canary test shows the old neutral fill failing.

    • Evidence: Material 3's surfaceContainerHighest step over surface; IBM Carbon's dark tag background step. The common dark-badge bug is a fill tuned for white that freezes on dark.
  • 2026-09-28 SHIPPED · EDIT-SHEET, EDIT-PENCIL, EDIT-PAGE-BARE, MAP-BAR-INK, MAP-NO-CHIP, ONE-BOTTOM-CTA, BADGE-DARK-FILL: commits 3fb3bc19…7f26b0e0, preview OTA a14ad2de, List 35 · yarn check green; web rung checked the bottom sheet (viewport capture), the pencil rows, the bare edit page, the one-CTA bottom and the dark-mode badges.

  • FACTS-LEAD-DETAIL (2026-09-28, owner: "because it's gotten so long, a user has to scroll down to see the participants … is there a way to collapse or compact things down to the bare minimum") · The session fact block shows two layers. The lead is 내 상태, 일시 (with 응답 기한), 장소 and 참가비, plus any warning (a late-cancel penalty, rain risk). The details, 종목, 대상 and a calm forecast, join the same list under a 자세히 보기 / 접기 link, closed on every visit. Measured on a 390×844 phone, 참석 moved from y=721 (under the CTA band) to 639, so its first rows are on the first screen.

    • Evidence: NN/g progressive disclosure ("disclose everything that users frequently need up front"; at most two levels); GOV.UK's single show-all toggle instead of per-row accordions; NN/g's fold research (the section after the hero must peek, not a false floor). Date, place and price lead on every mainstream event page.
    • Rejected: shrinking the 2:1 map (the owner asked for that size a day ago); a separate "N명 참석" summary row near the top (it would repeat the 참석 title's count); per-row accordions; remembering the expanded choice (the ask was compact by default).
    • Kept against the research: the research would also hide 취소 규정 and rain. They stay in the lead only as warnings, because a consequence shouldn't wait behind a tap.
  • 2026-09-28 SHIPPED (later) · FACTS-LEAD-DETAIL: commits ef4d6535…369b3300, preview OTA afc1ff71, List 36 · yarn check green; web rung measured 참석 at y=639 (was 721) on a 390×844 screen.

  • EXPAND-TOGGLE (2026-09-28, owner: "Don't we have a canonical pattern for collapse/expand? I would stay away from text-based control buttons at all. Make sure we implement the collapse/expansion properly") · Collapse and expand is now one primitive, ExpandToggle: a full-width 44pt row with a chevron that turns when opened, and no text link. With content it is an accordion header (the receipts' headlines). Without, it is a lone centred chevron under a shortened list (the session facts, tier credentials, the crash screen's details). What opens enters with the app's content reveal (ExpandReveal, or FactList revealedItems), and everything rests under reduced motion. The accessible name ("자세히 보기" / "접기") and the expanded state are announced to screen readers but never shown. DisclosureLink is deleted and all five uses moved, and LabeledField's collapsible row now shares the same rotating chevron.

    • Evidence: NN/g (the caret is the one icon that reads reliably as expand/collapse; progressive disclosure one level deep); GOV.UK's accordion (one toggle per block); M3 and iOS disclosure chevrons that rotate on toggle.
    • Rejected: keeping any text-only toggle (the owner's rule); per-row accordions in the fact block.
  • HERO-ZONE (2026-09-28, owner: "we may not need the divider between the top section (map, title, alert) and the information block because they together form the hero") · The session page's alerts, notice, live bar and fact block render in FlatColumn's lead slot as one stack, with no divider between them or above them. The first line on the page now opens 참석.

  • 2026-09-28 SHIPPED (latest) · EXPAND-TOGGLE + HERO-ZONE: commits aae47c70…03eeb8b7, preview OTA cc805948, List 37 · yarn check green (495/495 ui tests, all suites); web rung: facts closed and opened, no divider above them.

  • DISCLOSURE-SWEEP (2026-09-28, owner: "Take care of all remaining items in the canonical way") · Every in-place collapse/expand in the app now uses the one pattern. An audit found fifteen hand-rolled ones:

    • chevrons swapped by state;
    • rows with no expanded state for screen readers;
    • text toggles (전체 보기/접기 under the ranking, 일정 더보기/접기 on the club home, 설정 더보기/접기 in the tournament setup, 더보기/접기 in venue corrections).

    Rows became ExpandToggle headers; lists that were cut short got the lone chevron; a chip, a notification row shell and the sticky round headers keep their container and draw ExpandChevron. ESLint @twomore/no-hand-rolled-disclosure now errors on a chevron swapped or turned by state, and on a collapse word shown instead of sitting in the accessibility label. The public pickup preview's fact block joins its hero zone as on the member page (HERO-ZONE).

    • Evidence: NN/g's 2020 accordion-icon study (Laubheimer & Budiu): the caret beat arrows and plus signs, and label and icon must do the same thing. WAI-ARIA APG disclosure: a button with aria-expanded. M3 lists: an expandable item announces its state.
    • Also: the venue pick takeover's "테니스장 N곳 더보기" text button (25 more rows per press) became the lone chevron. It opens every loaded row and folds them back, and "N곳 더보기" is now its accessible name.
    • Rejected: a rule keyed on file names; leaving load-more text buttons as an exception.
  • 2026-09-28 SHIPPED (latest) · DISCLOSURE-SWEEP: commits d9dfb2dc…58483932, preview OTA 6c0882e2, List 38 · yarn check green (7,574 tests; the new lint rule finds 0 hand-rolled disclosures; 41 drifted boards resolved); web rung: FAQ chevron rows, club records, session hero zone.

  • FEE-SETTLED (2026-09-28, owner: "removing the 참가비 from sessions that you've already signed up for because you've already paid for it most likely") · The session fact block drops 참가비 once the viewer's spot is settled:

    • the host, who runs it and pays nothing;
    • a confirmed player whose payment is paid, waived or free;
    • a confirmed player who still owes it, because the payment banner above states the amount.

    It stays for browsers, waitlisted players (who may be asked to pay) and a confirmed spot with no payment record. Each case is pinned by a test.

    • Evidence: the model already carries viewerPaymentGate, the one canonical derivation, so no new state is involved. Showing only what the viewer still needs to decide is NN/g's progressive disclosure, and the owed case is never hidden, because the banner states the amount.
    • Rejected: hiding the fee for everyone who signed up (a waitlisted player can still be charged); hiding it for a confirmed spot with no payment record (nothing else would state the price).
  • CHEVRON-ROW-RHYTHM (2026-09-28, owner: "if we are rendering the chevron as a separate row, we should give it proper spacing up and below") · A lone chevron is its list's last row. Its box is the glyph plus an inset on the seam ladder, and the 44pt floor is hit slop. FactList owns its chevron (disclosure) on its own rhythm: 24px to the last row's text, which equals the gap between rows, and 24px to the divider. Measured on the web build: before, 15px above and 38px below; now 24 and 24. 참석 on the session page moved from y=640 to 588 with the fee change.

    • Also: the space scale and the icon size scale each moved to one framework-free module (space-scale.ts, icon-sizes.ts), so layout helpers read pixel values without hand-copied numbers.
  • NAMES-NEVER-CUT (2026-09-28, owner: "we shouldn't cut people's names. Other things should be cropped if we are crammed for space") · A person's name now wraps to two lines everywhere and is never shortened with an ellipsis. An audit found nine places cutting names to one line:

    • the shared player chip, which covers every ranking, standings and scorecard line and showed "정…" in the club ranking;
    • the host card;
    • the podium;
    • match history and the match timeline;
    • the king-of-the-court card;
    • the payment row;
    • top partners;
    • the member modal header.

    In the ranking row the dead spacers went first: the 24pt trailing column (나 now sits beside the name) and the blank 조 column in the 조별 view. ESLint @twomore/no-single-line-title now also flags a single-line <Text> rendering a person-shaped name, whatever its text role. Places and things (venue, club) may still crop.

    • Evidence:
      • Apple's larger-text evaluation criteria: wrap to two or more lines rather than truncate.
      • W3C "Personal names around the world": leave space for the name.
      • Hoober, UXmatters 2020, on mobile tables: remove or relocate columns, never the identifying one.
      • Duolingo leagues and Strava leaderboards keep one identity slot and one headline figure on the row.
    • Rejected:
      • a horizontal scroll with a frozen name column (the app avoids horizontal scrolls for your own data);
      • shrinking the name's font;
      • cutting the name with a detail sheet as the escape hatch (Apple allows it; the owner does not).
    • Open: moving 전적 and 변동 under the name in the ranking row (the research's full layout) is a further option. Removing the spacers already fits Korean names at 390pt.
  • 2026-09-28 SHIPPED (latest) · FEE-SETTLED + CHEVRON-ROW-RHYTHM + NAMES-NEVER-CUT: commits 3469a312…bb109a2b, preview OTA 481f7306, List 39 · yarn check green; web rung: session facts 24/24 around the chevron, fee row gone for a confirmed viewer (참석 y 640→588), ranking names in full (정성준, 권승민, 김하은).

  • CHEVRON-NAMED (2026-09-28, owner: "look for best practices and reflect them. We are always going for something clean and uncluttered") · A chevron that hides a different kind of content from what shows now names it, quietly. A chevron that continues a list stays bare.

    • Session facts: the row reads "종목 │ 대상 │ 날씨" in tertiary ink with the chevron trailing. Once open, the named rows are on screen, so the words drop and the chevron stays where it was. This is ExpandToggle summary, reached through FactList disclosure.
    • Tournament setup: the settings sentence that already sat above the chevron became the chevron row's own header, so no words were added.
    • Venue correction: the folded fields are named up to three, then counted ("이름 │ 종류 │ 코트 구성 외 6개"). EXPAND_SUMMARY_MAX sets the cap, ui.listAndMore holds the copy, and one title map (venue-correction/field-titles.ts) now feeds both the field sections and the summary.

    The visible words lead each toggle's accessible name (WCAG 2.5.3, label in name).

    • Evidence:
      • GOV.UK Details: the summary text lets people "quickly work out if they need to click".
      • NN/g icon usability: icons need visible labels.
      • NN/g 2020 accordion-icon study: even the caret was near a coin flip on "stays on this page", and only beat having no icon.
      • Baymard truncation research: continuation controls must stand apart from the rows they continue (the rhythm fix covers this).
    • Rejected: a label on list continuations (the cut-short list carries the scent); a text link (the owner's rule stands: the words are content, and the row with its chevron is the control).
  • RANKING-FOLD (2026-09-28, owner: "look for best practices and reflect them … clean and uncluttered") · A leaderboard row keeps one identity slot and one headline figure.

    • Club ranking row: [조 in 전체 only] · # · avatar · name (with 나) · ELO on the right. Under the name, a quiet second line shows the record in the canonical "N승 N패" format and the 7-day change (▲/▼ coloured, omitted at 0). The 전적 and 변동 columns and their header captions are gone.
    • Session standings: the ELO change moves under the name, and points stay the headline.
    • Records leaderboard: keeps its tier badge, which is its own tap target.
    • Code: PlayerIdentityChip meta and LeaderboardRow meta; the secondaryValue slot is deleted.
    • Evidence:
      • Hoober, UXmatters 2020, on mobile tables: relocate secondary columns and keep the identifying one.
      • Duolingo leagues and Strava leaderboards show rank, identity and one number per row, with the rest on tap. The ranking row already opens MemberStatsModal with the full figures.
    • Rejected: a horizontal scroll; abbreviated column captions.
  • PRESS-NO-BAND (2026-09-28, owner: "Is there a way to indicate that something is being pondered for action without rendering the grey band over the component? … We are always going for something clean and uncluttered") · Press and hover feedback now stays inside the thing pressed. The $surfaceTertiary state layer that row/card pressables painted edge to edge on press and on web hover is retired, along with the stateLayer prop (13 opt-ins removed) and the affordance value layer, renamed surface.

    • Press: the content settles and dims (VARIANTS, now in pressable-visual.ts): row 0.98/0.70, card 0.98/0.90, pill 0.96/0.85; button, icon and fab unchanged.
    • Hover (web, real pointers only): one gentle dim, HOVER_OPACITY 0.85, shared by Pressable and Button (they had drifted to 0.93 and 0.9), plus the pointer cursor.
    • Reduced motion: keeps the dim and drops the settle, now in Button too, which never gated it.
    • Focus: the keyboard focus ring is unchanged.
    • Gate: ESLint @twomore/no-press-tint forbids a background in any pressStyle/hoverStyle.
    • Evidence:
      • Apple keeps pressed feedback inside the control (UIButton dims itself); the full-cell grey is UIKit's list default, not a rule.
      • Material 3 tints only inside the component's own shape.
      • Toss's engineering team added motion so taps "feel pressed" (toss.tech). The post adds motion to a grey background; it doesn't say they removed it.
      • NN/g response limits: feedback within 0.1s.
      • Research warns that feedback weaker than about 0.99 scale or 0.9 opacity reads as nothing, so rows dim to 0.70.
    • Rejected:
      • keeping a confined rounded tint (still a band, against the owner's ask);
      • removing feedback entirely (users double-tap, and it fails low-vision and motor-impaired users);
      • a hover underline or colour change per component (it doesn't generalise across rows and cards).
  • 2026-09-28 SHIPPED (latest) · CHEVRON-NAMED + RANKING-FOLD + PRESS-NO-BAND: commits 243e1968…cb2cb7e4, preview OTA e7e5c480, List 40 · yarn check green; web rung: session facts row reads "종목 │ 대상" + chevron (accessible name "종목, 대상, 자세히 보기"); ranking rows "4승 4패 ▲40" under the name, ELO right.

  • SECTION-SEAM-8 (2026-09-28, owner: "there is a wider gap between the section and items on the club pages (my clubs and discovery) than in other pages") · Measured on the web build from the title text to the first card: 홈 8pt, 내 클럽 24pt, 탐색 20pt. The canon is 8pt everywhere. There were two causes, both fixed at the source.

    • 내 클럽 (a React Native trap in a shared list): GroupedFeedList drew its between-sections seam through RN's SectionSeparatorComponent, telling "between sections" from "under a title" by leadingItem. VirtualizedSectionList keeps those props in state set once per cell, so after 내 클럽 re-sorts, the first card's separator carried a stale club row. React's fiber props on the live page confirmed it. It then drew 16pt under the title. The seam is now a section footer after every section but the last, rendered from the live section. It also can't stick with iOS's sticky headers. Unit-tested in section-seam.test.ts.
    • 탐색: the list header stacked paddingBottom $3 on the title's own 8pt; removed.

    After the fix: 8/8/8. The between-section seam is unchanged (the ranking's 조별 groups still sit 16pt apart).

    • Rejected: patching the separator's leadingItem check (the props themselves go stale); per-screen negative margins.
  • 2026-09-28 SHIPPED (latest) · SECTION-SEAM-8: commits on main after 6a7babeb, preview OTA e924136a, List 41 · yarn check green; web rung: title→first card 8pt on 내 클럽 (was 24), 탐색 (was 20) and 홈.

  • PAGE-POLISH REVIEW, STAGES 0–1 (2026-09-29, owner: "Yes to all, but dev doesn't need to be handled too carefully. It's more of a mock that will be removed anyway") · Implements the independent review of the 2026-09-29 page-quality audit (.audit/page-polish-20260929/claude-review.md, F01–F20 + C01–C08). Stage 0 is the privacy, consent, onboarding and venue-identity work; Stage 1 is the shared interactions.

    • PRIVACY-PATCH (F04): a visibility chip used to write the whole client-merged profile_visibility object, so two quick edits could overwrite each other and an edit before the profile loaded wrote the defaults. Now patch_profile_visibility(field, level) (00675) merges one field server-side (pgTAP 12/12); the chips stay disabled until the read is ready and online.
    • CONSENT-NOT-BUNDLED (F05): the 알림 수신 push toggle also granted/revoked the marketing_push PIPA purpose — consent bundled into a functional switch. The toggle now only sets delivery. 00676 appends withdrawal rows for every active settings_toggle marketing grant (owner decision 1; signup_screen grants kept). ESLint consent-api-only-on-consent-surfaces keeps the consent-write hooks on the consent-management screen and the onboarding consent step.
    • CONSENT-ACTIVE-PREDICATE (C08, found while doing F05): the append-only ledger never let a withdrawn user re-grant — the closed grant still read as active by state alone, and a unique index allowed one grant row per purpose for all time. The 2-year expiry cron would also have thrown on its first run. 00677: private.active_consent_id is the one active predicate, an insert guard replaces the index, and all four writers read the predicate; pgTAP 16/16, full suite 153 files / 3,371 tests. Canon: korean-market.md.
    • ONBOARDING-REVALIDATES (F06): finishing onboarding stamped the terms/privacy acceptance without re-checking anything, so going back and unchecking a required consent still produced an "accepted" profile. One pure validator (firstInvalidOnboardingStep) now drives both forward navigation and finish; finish sends the user to the first invalid step. Canon: wizards.md "Revalidate at finish".
    • VENUE-FACTS (F13a–f):
      • Venue detail's 길찾기/지도 handlers were crossed.
      • The directory's open-now label ignored overnight hours; it now reads the domain isVenueOpenNow.
      • Every never-observed venue read "야외"; indoor/outdoor is now tri-state (deriveDirectoryVenueIndoorOutdoor, unknown = omitted).
      • The public pickup preview reported the minimum shortfall as spots left; it now shows maxPlayers − confirmed, plus a separate more-needed fact.
      • The directory's host-a-game preset sent the Korean region label where a slug belongs.
      • A pickup hosted at a searched venue now bridges to that venue's real record (ensureClubCourtVenue) instead of a coordinate-less stub.
    • PILLNAV-INDICATOR (F02): on web the selected-tab indicator stayed on the old tab (reproduced at HEAD). The web build has no worklet plugin, so useAnimatedStyle without explicit dependencies never re-ran; PillNav and CollapsingHeaderLayer now pass them. ESLint reanimated-explicit-deps holds the class.
    • A11Y OWNERS (F03):
      • PillNav exposes tablist/aria-selected through accessibilityStateProps.
      • A named AvatarBubble is an image.
      • The web viewport no longer blocks zoom (WCAG 1.4.4).
      • ProgressBar requires an accessibilityLabel; all 16 consumers pass the fact already beside the bar ("참석자 (3/8)").
      • On web, Pressable realises hitSlop as an invisible out-of-flow child, so icon buttons get their 44pt target. The prop used to leak into the page as hitslop="[object Object]" and do nothing.
      • The session card's save toggle is no longer a button inside the card's button, which left it unreachable to screen readers. It renders as a sibling over an empty slot of its exact footprint, placed by summing the measured offsets on the slot's path; web probe: toggle rect equals slot rect on all four home cards. It uses aria-pressed, not aria-selected; native keeps the selected trait.
      • A test with a canary (it fails against the nested version) pins the sibling structure.
    • WEB-LOCATION (F17, owner decision 5): Permissions-Policy allows same-origin geolocation, and the web expo-location stub asks the browser only on an explicit request; passive reads stay passive.
    • Rejected: client-side retry-merge for privacy edits (still races); keeping bundled consent with a clearer label (PIPA purpose-specific consent); deleting ledger rows to "fix" consent (append-only is the legal record).
  • 2026-09-29 SHIPPED (latest) · PAGE-POLISH REVIEW, STAGES 0–1: commits 872ec1be…55615c4f, preview OTA dbcb379e, List 42 · 00675–00677 on the hosted QA project (live function definitions matched the local ones byte for byte before the push; after it, the one bundled marketing grant is withdrawn and the insert guard is in place), seed-scenario redeployed · yarn check green; pgTAP 153 files / 3,371; web rung: PillNav indicator follows 주/월, save toggle rect equals its slot on every home card, no hitslop attribute in the DOM, progress bars named.

  • UPGRADE-PHASE-A (2026-09-29, owner: "As for keyboard and other libraries and modules in place, I do recommend updating to the latest versions taking care of compatibility carefully") · Supersedes the review's decision 4 (keyboard-controller as a documented exception). The plan: .audit/page-polish-20260929/claude-review-assets/upgrade-plan.md. Phase A stays inside SDK 55.

    • Native (app version 0.8.0 → 0.9.0; EAS preview build a55c851f): expo 55.0.31 and its modules on the SDK 55 table; react-native 0.83.10 in every workspace pin; keyboard-controller 1.22.5 (dormant); one expo-constants.
    • Kept off Expo's table on purpose (expo.install.exclude): flash-list 2.3.2 (the Fabric recycling fix); keyboard-controller (owner: latest); TypeScript 6.0.3 (SDK 56's own version; every workspace typechecks).
    • JS: next 15.5.26 (hardening for GHSA-vcvr-r3jv-pc5j; 15.x was never affected); supabase-js 2.117.2; TanStack Query 5.104; zod 4.6.5; zustand 5.0.15; typescript-eslint 8.71; knip 6.38; jscpd 5.3.3; prettier 3.9.9; supabase CLI 2.118.
    • Held: @react-navigation/native 7.2.2, because expo-router locks that copy and a second one splits the navigation context under the focus and leave-guard hooks. Also held: lint-staged 17 (needs Node 22.22.1) and @types/react 19.3 (runtime is 19.2).
    • Node: CI 22 + 24; engines >=22.12.0.
    • Verification:
      • yarn check green;
      • web production build green (the two import warnings predate this change — F08);
      • Android Hermes export 15MB;
      • expo-doctor clean except the pre-existing monorepo Metro override.
    • Next: Phase B (SDK 56 in code, ship on SDK 57) after the owner installs 0.9.0; Phase C majors each on their own.
    • Rejected: expo install --fix wholesale (it would downgrade flash-list to the pre-fix 2.0.2); silencing TS 6 deprecations with ignoreDeprecations (TS 7 removes baseUrl anyway).
  • UPGRADE-PHASE-B (2026-09-29, same directive as Phase A) · Expo SDK 55 → 56 in code → 57 shipped: app version 0.10.0 (new runtime). SDK 56 alone isn't shipped: its Hermes v1 has a reanimated memory bug that SDK 57 fixes.

    • Versions (all from expo install --fix against Expo's table): React Native 0.86.3, React 19.2.3, reanimated 4.5.1, worklets 0.10.1, gesture-handler 2.32, screens 4.26, datetimepicker 9.1, babel-preset-expo 57.0.13, @react-native/jest-preset 0.86.3.
    • Expo Router forked React Navigation (SDK 56). Expo's codemod moved NavigationContext to expo-router/react-navigation. It now reaches the focus and leave-guard hooks through @/navigation/app-router, so ARCH-4's single importer holds. The standalone @react-navigation/native is removed: a second copy's context would never be provided, so every screen would read as focused and every unsaved-edit guard would be off.
    • Web:
      • expo-router/react-navigation has its own shim (no navigator on web).
      • reanimated and worklets now ship "type": "module", and webpack's strict ESM left their web path without require and global: every animated view crashed.
      • One module rule parses those two packages as javascript/auto, as before.
      • The web rung now prints the page's errors when login never hydrates.
    • Gate: check:expo-sdk-table compares every workspace's native modules with Expo's table. The mobile preflight reads only apps/mobile, so five Expo modules declared in packages/app stayed on SDK 55; a canary test pins that case.
    • Type fixes: StyleSheet.absoluteFill; tab icons take ColorValue; the navigation ref's route narrows to its name.
    • Rejected:
      • Keeping @react-navigation/native beside the fork (a silent no-op context).
      • DefinePlugin({ global }) alone (it hid the require half of the web break).
      • Shipping SDK 56.
  • WEB-FRAME + ROUTE-PARAMS (Stage 2: F01, F12) (2026-09-29, same owner approval; desktop web is a target, owner decision 3) · The web shell is fixed to the viewport, and a route's params have one parser.

    • F01, the web frame sized to its content: the tab bar sat below the fold on long pages and mid-screen (y≈540) on short ones. Now there is one [data-web-frame] box (100dvh, no document scroll) and a page slot keyed by path that scrolls only a shell-less page. The tab bar is in flow, and Freeze's web fallback passes its size through. Next's React has no Activity, and the flexless fallback had let a tab pane's list grow to its content.
      • Measured on the web rung: document height 844 = viewport on 8 routes; the tab bar ends at the viewport bottom; /clubs scrolls inside its list.
      • Contract test for each clause.
      • Desktop: the centred 480px column stays. Side navigation is a design question for a wireframe, not this fix.
    • F12, route params: web's join-club dropped the originating club, web's create-session dropped the three venue presets, and the directory CTA appended query strings to a built URL by hand. route-params.ts now holds one parser per route beside its builder; all four wrappers spread it, routes.createSession(clubId, presets?) builds every preset, and the 'pickup' literal is PICKUP_CLUB_SENTINEL. A round-trip test plus a parity test cover the wrappers.
    • Rejected:
      • Sticky chrome over document scrolling (web would stop matching native's list virtualization and collapsing headers).
      • A fallback ScrollView page (an unbounded content container can't bound the shell's own ScrollView).
      • body > div (it also sized the stray empty divs).
  • 2026-09-29 SHIPPED (latest) · UPGRADE-PHASE-A/B + WEB-FRAME + ROUTE-PARAMS: native builds a55c851f (0.9.0, SDK 55 latest) and 2a544fb5 (0.10.0, Expo SDK 57 / React Native 0.86), both emulator boot-smoked. The preview baseline now sits on 0.10.0, and OTA 0f6c2142 (Stage 2) runs on the emulator. Phone QA: lists 43–45. yarn check green on fresh Jest caches.

    • Found on the way:
      • check:clones never scanned the web app (C09);
      • five Expo modules outside apps/mobile stayed on SDK 55 (now check:expo-sdk-table);
      • reanimated's new ESM build broke every animated view on web;
      • a stale Jest cache hid a Babel-preset change (the cache is now keyed by yarn.lock).
  • PAGE-STATE + DRAFTS (Stage 3: F10, F11) (2026-09-29, owner: "Let's proceed with your recommendations") · A page's conclusion waits on every read it depends on, and server sync never overwrites what someone is typing.

    • F10: useCompositeNetwork gains refetch(), which awaits every query, and is the one composer for a screen's critical reads.
      • 내 클럽 (a guest-only member no longer sees "no clubs" while the guest read is pending or failed).
      • 저장됨 (a failed saved-id read isn't "nothing saved"; a genuinely empty list doesn't pin the skeleton).
      • 기록 (the stats read joins the boundary: it used a different cache key, limit 200 vs 50, and four reads with no representation).
      • The host dashboard's applicants (error or offline, never "no applicants").
    • Discover refresh returned void, so the spinner stopped at once; it now awaits both reads.
    • F11:
      • useServerDraft (new, @twomore/app): hydrate on arrival, re-hydrate only while pristine, markSaved, and a reset that adopts the latest server value. The bank form uses it (no clobber on refocus; no save unless the read succeeded).
      • Create-session: rules edits (and the last-used-rules default) go through the persisting setter.
      • Leave guards: the contribution editor, the owner-claim application and the claim review actions get useUnsavedChangesGuard with a real dirty flag, released before navigating after success.
    • Found in passing: session.findByIds put hundreds of ids in one GET URL; past the gateway's limit the browser reports "Failed to fetch" (/records/history for a busy account). All unbounded .in() reads now go through one batching helper, and a lint rule prevents new ones.
    • Rejected: a per-screen Promise.all for refresh (it's the helper's job; a void wrapper is the bug); keying the bank draft on a dirty flag inside the form (the hook owns the rule once).
  • READABLE CONTENT (Stage 4) (2026-09-29, same approval) · Decision content is read whole before its action, and each fact appears once.

    • D6: ListRow.subLines and PersonRow.subLines, with EXPLANATION_LINES (4). Used for the report reason, the quiet-hours and payment helpers, the two attendance-policy helpers and the host-confirm hint. Metadata stays one line.
    • F15: resolving a report now confirms with the whole report (subject, reporter and date, category, the full reason) before the consequence.
    • F20: policy tables pass wrapping values (FactRow's ReactNode branch). SectionBlock.titleLines lets a legal article heading wrap: 0 clipped texts on /legal/privacy and /legal/terms.
    • C04: Korean breaks between words. iOS uses lineBreakStrategyIOS: 'hangul-word' on Text; web uses word-break: keep-all plus overflow-wrap: break-word on .is_Text (224/224 texts, no overflow). Android has no RN prop yet.
    • C02: records history takes the list's rail (railedList, rows rail={false}, list + flush month headers, as the members list does).
    • F09: the challenge sections are gated at the call site (no blank divided bands). Directory 시설 uses one extraVenueCapabilities for the section and its visibility. The public preview's gallery renders nothing when there are no photos: EMPTY-ONCE is canon, so this needed no new ruling. Veto-able.
    • F18 (measured): a nonmember's capacity row is information, not a disabled control, and the member count appears once.
    • S9: comments and join-request messages use AutoLinkText.
    • S10: member stats show ··· while unknown, never a measured 0% or 0/0.
    • S11: album photos keep their alt text or caption.
    • F14: the owner-claim screens pin their bottom band through DetailShell.footer (OwnerFrame footer). The venue picker mounts with OwnerFrame list: no second rail and no ScrollView around the finder's virtualized list (the courts directory's scroll={false} mount).
    • Board for the owner: https://twomore-cockpit.pages.dev/wireframes/page-polish-open (post title vs author hero, partner-candidate density, analytics as fact rows, desktop centred column vs side-nav). Each question carries a recommended path.
  • ENGINEERING GATES (Stage 5) (2026-09-29, same approval) · Each fix lands at its owner, with a check that fails on the next occurrence.

    • F16: create-club's extra meets go through useCreateRegularMeets (one mutation, per-item capture, club caches invalidated). The barrel no longer exports getSupabase, regularMeets, payment or edgeFunctions: the privacy export uses useExportUserData, web login useAuth().signInWithEmail, the dev reset resetOnboarding. ArchUnit derives the feature list with a canary (it had covered 6 of 8).
    • F17: one owner for the web Sentry options (sentry-web-options.ts). The re-audit caught the configs passing process.env whole, which is empty in the browser, so client Sentry would never have started. They read literally through apps/web/lib/sentry-env.ts, and no-dynamic-process-env holds the class (it also removed the share adapter's never-populated process.env[name] fallback).
    • F19: one share-page style owner; the subtitle is #4b5563 (7.06:1). /design-system/* may be framed by the same origin (SAMEORIGIN + enforced frame-ancestors 'self'), so /preview renders; everything else stays DENY.
    • C07: /status 404s on production builds (Vercel previews excepted). C01: the web dev login is inlined only in non-production builds.
    • C09: the share-preview types and pure parser live once in domain/public-share-preview.ts; clone baseline 1,206 → 971 lines.
    • C03: no-raw-accessibility-object-prop is COMP-12's planned rule; zero violations after F03, canaried under the real config.
    • Batching: selectInBatches + no-unbatched-in-filter, with no per-file exemption.
    • F08 / C05: images.unoptimized (no page uses next/image, so /_next/image 404s before sharp; a test holds it). The dependency refresh (51 high + 2 critical → 3 high, each a dated exception) and the check:deps-audit gate are parked on branch deps/security-refresh: the OTA guard treats any app-reachable lockfile change as native, so they land with the next native build.
    • Upgrade Phase C, by evidence. Go: Next 16 + @sentry/nextjs 11 (web), MMKV 4 (native, with the refresh). Hold: gesture-handler 3 and Sentry RN 8 (outside Expo SDK 57's table), Jest 30 (jest-expo 57 is on babel-jest 29), ESLint 10 (the react, jsx-a11y and import plugins stop at 9), TypeScript 7, lint-staged 17.
    • Tamagui 2: hold for a branch spike. Stable icons ship under a new package name (~300 imports, possible renames), v2 replaces RN accessibility props with aria (~1,100 sites, silent if wrong), and the v5 layout defaults need a visual pass. A third party reports 2.7.7 working on our exact stack.
    • SHIPPED: Stages 3–5 in preview OTA 88456c6d on 0.10.0 (2026-09-29); QA List 46.
  • UPGRADE-PHASE-C (2026-09-30, same directive as Phase A) · One native cycle, app version 0.11.0 (new runtime), carrying everything the OTA guard can't ship.

    • Security refresh: the parked deps/security-refresh commit (stale resolution pins bumped, yarn up -R; 51 high + 2 critical → 3 high, each a dated exception) and the check:deps-audit gate (C05). Next 16 then removed its exact postcss pin, so the gate reported both postcss exceptions stale and they were deleted: one dated exception remains (vite, docs site only).
    • Next 16.3.7 (webpack): next dev/next build take --webpack because the app keeps a custom webpack config (react-native-web aliases, the strict-ESM rule). Every server page and OG route already awaited params; image defaults don't apply (the optimizer is off). The web expo-router shim gains Redirect (the shared TabAliasRedirect resolved to undefined on web).
    • @sentry/nextjs 10.75.3, not 11: v11 drops sendDefaultPii and, with dataCollection unset, collects cookies, bodies, user info and DB queries — a PIPA decision, not a bump — and mobile stays on the v10 core (Expo SDK 57 pins Sentry RN ~7.11), so the one shared web options owner (F17) would straddle two majors. Revisit with Sentry RN on v11.
    • MMKV 4.3.2 + react-native-nitro-modules 0.37.1: createMMKV, .remove (v4 renamed delete); the same instance id keeps existing data. Nitro 0.36–0.37 list no breaking changes after 0.35.9 (MMKV's build target).
    • Held, by evidence: gesture-handler 3 and Sentry RN 8 (outside SDK 57's table), Jest 30 (jest-expo 57 is on babel-jest 29), ESLint 10 (react, jsx-a11y and import plugins peer ≤ 9; note root already pins @eslint/js ^10 beside ESLint 9), TypeScript 7, lint-staged 17, Tamagui 2 (branch spike first).
    • Rejected: loosening the OTA guard for web-only lockfile changes mid-cycle (a guard change deserves its own review; the fingerprint-based guard is the better follow-up).
    • SHIPPED: EAS preview build e4583905 (0.11.0, compiled from 13a0fa5d); emulator boot-smoked (Nitro MMKV loads, stored session kept); OTA baseline advanced; QA List 47.
  • UPGRADES GO (2026-09-30, owner: "let's work on all the recommended changes") · TypeScript 7 for yarn typecheck only (47.8 s → 6.8 s on @twomore/app, zero errors; lint stays on TypeScript 6 because typescript-eslint caps it below 6.1), a fingerprint-based OTA guard (@expo/fingerprint, so web-only lockfile bumps stop forcing native builds), and the Tamagui 2 branch spike. Gesture-handler 3, Sentry RN 8 and Sentry 11 still wait for the next Expo SDK.

    • TypeScript 7 gate SHIPPED: yarn typecheck runs the native compiler over 13 workspaces in 34 s (was 6 min 44 s); canary test; .claude/** (agent worktrees) excluded from ESLint after one inflated the gate to 8,990 findings.
    • Fingerprint OTA guard SHIPPED: the guard's primary proof is now Expo's native fingerprint of apps/mobile (default options), compared with the hash EAS recorded for the installed build (preview.nativeFingerprints.android = 2f488e4d… for e4583905; the local hash matched exactly). Measured: a web-only next bump or a lockfile-only edit leaves it unchanged; a native module declared in packages/app or apps/mobile changes it. When it can't be trusted locally (dependencies not installed, Kakao/google-services env set, google-services.json missing) the old manifest check decides. So web and JS-only upgrades ship as OTAs, and only real native changes need a build.
    • Tamagui 2.7.7 (JS only; its native fingerprint equals build e4583905, so it ships as an OTA): config v5 with styleCompat: 'legacy' and defaultPosition: 'relative' (the v1 flex and position behaviour), View for the removed Stack, transition for animation, icons from @tamagui/lucide-icons-2 (409 imports, no renames), moti driver kept for now. v2 translates no accessibility props on either platform, so the codebase moved to the web vocabulary React Native also reads: aria-* and role (541 labels, 135 roles, states and values; 48 component APIs), helpers emit aria-* on both platforms, @twomore/no-rn-accessibility-props bans the RN forms (accessibilityHint/Actions stay RN-only). Text's typography variant role became textRole (1,163 sites) because it collided with ARIA role on the same element. Tests read state through RNTL matchers; @testing-library/jest-native removed.
    • Tamagui 2 web rung (8 routes vs main): aria-* present throughout (411 labels on 코트), zero leftover accessibility* DOM attributes, zero console errors after two fixes. (1) v2's web Image is a plain <img>, so source={{ uri }} never loaded and never fired onError — venue thumbnails were blank and the static-map fallback never ran; four sites moved to src, and @twomore/tamagui-image-src holds it. (2) Reanimated always hands its child a nativeID key, which v2 forwards to the DOM; Pressable drops it on web. The remaining ~1% pixel drift is text: v1 never applied our font letterSpacing tokens on web (−0.1 px body, "matches Toss look"), v2 does — web typography now matches the tokens and native.
    • SHIPPED: Tamagui 2 in preview OTA aca045c1 on 0.11.0 — the fingerprint guard passed it with no new build; emulator smoke (클럽, 기록, 설정 render; no crash-buffer entries; no JS errors); QA List 48.
  • FIX-WHAT-WE-CAN (2026-09-30, owner: "Let's fix what we can") · The items not blocked on an outside release.

    • Heading levels (review F03's open part): aria-level 1 on the screen title (MainTabShell, AppHeader, CollapsingTopBar), 2 on hero titles and SectionHeader (moved from the row, which also holds the action, to the title text), 3 inside a section; rule heading-has-level.
    • RN-only accessibility on web: hints go through accessibilityHintProps (aria-description on web), and every slider through adjustableActionProps — native keeps RN's actions; web gains arrow-key stepping, which web sliders never had. no-rn-accessibility-props now flags the raw forms.
    • Venue card: rating and review count wrap as one fact.
    • Lint dependencies: @eslint/js aligned to ESLint 9 (it was pinned to 10).
    • Animation driver: moti (deprecated in Tamagui 2) → @tamagui/animations-reanimated.
    • F07 re-checked: already closed (ef1403b5 pickup venue; F13e passes the canonical regionSlug).
    • Still blocked: gesture-handler 3, Sentry RN 8 and Sentry 11 (next Expo SDK), Jest 30 (jest-expo), ESLint 10 (plugin peers).
    • SHIPPED: preview OTA 5f531fd6 on 0.11.0 (fingerprint guard: no build); a new joi advisory caught by check:deps-audit was pinned past (17.13.8); emulator: 홈 → 주 indicator glides on the Reanimated driver, no errors; QA List 49.
  • BOARD ANSWERS (2026-09-30, owner: "partner candidates is a great addition … count as fact rows, yes … turn the web into a proper web app, not a mobile app being rendered") · Q2 and Q3 built; Q4 reversed my recommendation (centred column) and opens the desktop-web program (wireframes first); Q1 (titled post hero) still open.

    • Q2 partner candidates: one compact row per candidate — static PersonRow (tier │ ELO, the strongest reason) inside an ExpandToggle accordion header, the score trailing; the four factor bars, the shared record + trust tier and a 프로필 보기 row open through ExpandReveal, inset to the name column. The reason is strongestPartnerFactor (domain, tested): the factor highest against its own maximum, chemistry only with shared history. PARTNER_FACTOR_MAX replaces the repeated 40/30/15/15. Six candidates fit where two did. Canon over the mock: the board drew a centred chevron under each row; with a tappable identity row that would have meant two chevrons per candidate, so the row itself is the header and the profile link moves inside.
    • Q3 analytics: 탐색 and 주요 경로 counts are FactList rows (label left, tabular number right); the period moved to the section meta (recentDays, "최근 N일").
    • SHIPPED: Q2 and Q3 in preview OTA 08fb8f05 on 0.11.0; QA List 50. Also: the web typecheck now reads no Next dev-server artifacts (a half-written .next/dev/types file had turned the gate red).
  • WEB APP (2026-09-30, owner: "Let's go with your recommendations. Make sure you follow best practices for the libraries and design principles we are following.") · Board W1–W5 (https://twomore-cockpit.pages.dev/wireframes/web-app) built as drawn, plus Q1. Material 3 window size classes; Next's Link and nested layouts; Tamagui media props compiled to CSS.

  • WEB APP ROUND 2 BOARD (2026-10-06, owner on the 1440 build: "can be better adapted to the web UI/UX") · Proposal board /wireframes/web-app-2; research docs/design/research/desktop-web-adaptation-2026-10-06.md. Five questions, one recommendation each, nothing built:

    • X1 one 64px toolbar row (title+count, period controls, search/action) via a MainTabShell toolbar slot · X2 session cards become 64px one-line rows at 840+ (grid densified is the drawn alternative) · X3 detail = 48px breadcrumb strip + 680 column + 340 inspector holding the 16:9 map, key facts, fee and action · X4 pointer density from $expanded (rows 40/36, body 16 to 15, meta stays 13, lift+ring hover, / j k Esc ?; tokens cheap, keyboard layer new) · X5 profile in two columns, settings unchanged.
    • Backed by M3 supporting pane/feed/list-detail (360dp panes), Apple HIG toolbar and inspector anatomy, Strava's two-column profile, Linear's j/k and density; Meetup and Band publish no layout values.
    • W1 navigation: under 600 the bottom bar (only on the five tab roots, as on the phone); from 600 an 80px rail, from 1200 a 240px labeled sidebar. It lives beside the page slot, so it never remounts. Destinations are real links (next/link through Tamagui render, aria-current). The create action is a button, because it starts a task. 알림/메시지 carry the shared counts. Hidden on login, legal and share pages. Skip link first (WCAG 2.4.1).
    • Deviation from the board — the create action is 번개 만들기, not 일정 만들기: a club session is created from its club; the only global create on the phone is 경기's 번개 band. That band, the home bell and 프로필's 메시지/설정 icons hide on the web from 600 (WebNavCovered, one entry in one place). Native tablets keep them, since they have no web nav.
    • W2 tab roots: the column caps at 1200. 홈, 경기 and club discovery card feeds become 2–3 column grids, measured on the list (min column 320). A phone is untouched: one column is byte-identical. Controls (the segmented strip, the period pager, the search bar) stop at 480/680. 프로필 is a page read top to bottom, so it keeps the 680 reading column.
    • W2 home summary pane: not built. The grid already uses the width, and the pane's 이번 주 counts (참석 / 예정 / 입금 대기) would be a new product surface with no existing projection. It needs its own board question rather than an invented summary.
    • W3 detail pages: a 680 reading column; the scroll view spans the page, so the wheel scrolls anywhere. Headers keep a full-width fill and line their glyphs up with the column. A page with aside docks a 340 supporting pane from 840 with its summary and the footer's action; the footer mounts once, and the band sheds its chrome in the pane. Session detail is first: fee plus the card's RecruitmentMeter, a projection of the page's model.
    • W4/W5 list-detail: 메시지 and 설정 keep their list in a 360 pane beside the open page from 840. The open row is marked (ListRow selected), and the page slot keys by section so the list survives switching. A page the nav opens directly drops its back button on the web (navRoot).
    • Deviation from the board — 설정 uses the same list-detail as 메시지 rather than a separate 240 sub-nav: one pattern, and the settings rows already are the navigation. Wizards keep a centred 640 column, header and CTA included.
    • Size to the column, not the window: useContentWidth plus shells measuring through ContentColumn, so only width readers re-render. Heroes, the album pager and grid, and the onboarding pager moved. @twomore/no-window-width-in-layout holds the class. Breakpoints live once in window-size.ts, and the frame CSS is checked against them.
    • Native: the same media reaches native through @tamagui/react-native-media-driver, which is pure JS; the Android fingerprint is unchanged (2f488e4d…), so this ships as an OTA. Phones sit under every cap and breakpoint.
    • Q1 titled posts: a post with a title leads with it (TypeHero: club eyebrow, title, author and time as facts), and the in-body title is gone. Untitled member posts keep the author hero.
    • Found on the way (wizard): the session and club wizards wrote their persisted draft inside a React state updater, which React runs during render; opening the wizard logged "Cannot update a component while rendering a different component". One hook now owns the pattern, usePersistedWizardDraft: the next draft is computed from a ref in the event path, and the draft still persists on every mutation. Two nested step setters (onboarding, use-wizard-navigation) were moved the same way. @twomore/no-side-effect-in-state-updater holds the class.
    • Found on the way (gates): the i18n dead-key scan kept its own folder list without apps/web/components, so the web nav's strings read as dead. scripts/lib/app-source-roots.mjs is now the one list, shared with the clone scan, and a test fails when a web source folder is missing from it.
    • Found on the way (OTA guard): the fingerprint path judged dependency freshness by node_modules/.yarn-state.yml, which Yarn rewrites only when the node_modules layout changes. Adding the already-hoisted media driver changed the lockfile without changing the layout, so the guard fell back to the manifest check and refused a JS-only OTA. It now reads the newer of that file and .yarn/install-state.gz, and a test pins the case.
    • SHIPPED: preview OTA ee2b6a90 on 0.11.0 (fingerprint matches build e4583905, no new build). Web rung at 390/700/1000/1440. Emulator boot smoke: the update applies, 클럽 and 홈 render as before (bell kept on native), no JS errors. QA List 51.
  • CODEX LANDING D1 (2026-10-06) · First slice of Codex draft PR #15, landed by area (privacy/authority).

    • LANDED: migrations 00678 (friendship + home-signal authority), 00679 (private club-post-media bucket, signed bulk grants), 00680 (profile location visibility, computed-progression authority: update_player_progression is service-role only), with their pgTAP files and the edited 00002/00026 tests. Client side of 06b1262a3: signed-URL post photos (useClubPostMediaUrls/ByList, storage createSignedUrls, persist-deny for the grant key).
    • FIXED ON THE WAY: /status read adm1_slug raw from profiles (revoked in 00680), now reads through get_own_profile; the four client update_player_progression methods (no callers) are deleted from the progression port and adapter; the hand-typed clubPostMedia query key moved to clubKeys.postMediaGrants.
    • VERIFIED: local pgTAP 158 files / 3616 tests pass after migration up --local; typecheck, app and clubs tests, lint, check:static, docs:check. Deploy order: migrations before any function deploy.
  • CODEX LANDING D2 (2026-10-06) · Second slice of Codex draft PR #15 (scoring and competition), stacked on D1.

    • LANDED: 00681 full-set score corrections (score_revision, set-list proposals, consent-gated apply), 00682 rating serialization and event-time-preserving replay, 00683 manual-round completion and live-forfeit substitution, with their client counterparts (match-score-call mapper/adapter, session-match-sets cache, correction sheet, generate_session_round/complete_session wiring) and regenerated types.
    • LOCK REDESIGN: Codex serialized rating replay, every roster write and every departure behind one key (2147483000, 682), so any two sessions' RSVPs queued behind each other and behind scoring. Replaced with per-domain keys from 00681 (lock_rating_replay, lock_session_roster(session), lock_departures, plus the lock_match_score_write entry helper); roster guards became ROW triggers locking their own session; order is replay, departures, session roster, then rows. The upgrade-forbidden check stays only on the replay key. Table and rules: canon data-and-hooks "Write serialization"; 00681_write_serialization.test.sql asserts the held keys per entry point.
    • FORMAT LITERAL: session_matches_can_complete no longer lists king_of_court/mexicano/americano; it reads sessions.auto_complete_matches, which generate_session_round sets and 00683 backfills once (the only SQL copy). TS mirror INCREMENTAL_ROUND_STYLE_IDS is held equal by incremental-round-styles-parity.test.ts.
    • NOT LANDED HERE: scripts/db-concurrency probes and the seed truncate lock line (D3); only private.live_forfeit_promotions was added to the seed truncate list because 00683 creates it. Residual risk: UPDATE/DELETE row guards fire after the tuple lock, so a direct write racing a score on the same session can deadlock (40P01, one side retries); RPC entry points pre-lock to avoid it.
  • CODEX LANDING D3 (2026-10-06) · Third slice of Codex draft PR #15 (payments, sessions, messaging, notifications, public ingress), stacked on D2.

    • LANDED: 00684 paid-after-cancel refund transition + settle_manual_session_payment, 00685 atomic bounded session series + public attendance count, 00686 target-bound invite join, 00687 messaging authority and leave, 00688 atomic push claims and receipts, 00689 public ingress bounds, with their pgTAP files, the edge workers (send-push, check-push-receipts, seed-scenario caller check, fetch-holidays, export-user-data paging, shared ingress guard), the client counterparts (manual-payment binding, series submit, DM outbox, batched signal preferences, join expectedClubId) and scripts/db-concurrency/** with its CI step.
    • LOCK KEYS: Codex's lock_rating_state / lock_session_roster_write_state do not exist here; mapped onto the 00681 helpers. Payments are ROW-trigger roster writes (lock_session_roster); new in 00684: lock_dm_thread (2147483003, hashtext(thread)) (takes departures shared first), lock_push_claims (2147483004, 1), lock_refund_tick (2147483005, 1) (tick also takes departures shared and each overdue session's roster key). The read-receipt UPDATE skips rows a retraction owns (SKIP LOCKED). Table in canon data-and-hooks; 00684_write_serialization.test.sql asserts the keys.
    • IP DEFAULT: trustedClientIp returns the gateway-appended LAST x-forwarded-for hop when TWOMORE_TRUSTED_CLIENT_IP_HEADER is unset (value = another proven header, or unknown); only a missing/malformed header falls to unknown-ip, still capped per source and by the global daily cap. The DB policy default follows (x-forwarded-for-last).
    • HOLIDAYS: a provider failure releases its admission (holiday-provider-release), so only successes count toward the 64/day cap; 503 + Retry-After kept, 60 s per-year retry pause, empty-list 200 without the key.
    • PROBES: every probe refuses unless the target is loopback AND private.disposable_database() exists (created only by the CI pgtap step). Local run on the dev DB: 6 of 7 pass; raw tuple-first writers (cron auto_advance_session_statuses, raw session/match UPDATE) can deadlock with a score (40P01), left as a follow-up.
    • DEPLOY ORDER: migrations 00684-00689, then send-push, check-push-receipts, seed-scenario, fetch-holidays, export-user-data, then OTA. The web /api/vitals route (lane C) must ship with 00689: the new guard rejects the old payload.
  • CODEX LANDING E (2026-10-06): client-only repairs from Codex PR #15, no migration needed.

    • Landed: request deadlines held through body completion and native admission (request-fetch.ts, request-response.ts); queueable writes serialized per account/entity with durable replay (mutation-entity-sequence.ts, bounded replay retry, latest-reaction ordering); complete participation/history reads with a request fence and foreground/pane refresh; session readiness no longer sends a failed profile read to onboarding; locally owned mutation errors captured once; attendance actions and pickup settings preserved; comment cursors and reaction replay refresh.
    • Response-wrapper decision: Codex patched the native Response, its body stream and readers with Object.defineProperty. Replaced with a Proxy view returned to the caller: the native objects are never mutated, instanceof Response holds, extracted methods keep native brand errors. Trade-off: the view's body is not itself a native ReadableStream (its own methods work; new Response(view.body) would not), and nothing in supabase-js does that. Tests cover the Web stream path, the buffered no-body path and the RN fetch polyfill, and assert no own property is added to the native objects.
    • Dropped hunks (belong to later lanes): score_revision in the match mapper/columns and generate_session_round in match.supabase.ts (scoring/competition migrations); create_session_series and operationKey in use-host-pickup-game.ts/use-submit-session.ts; the public-attendance guest test and participantProfiles read; use-dm-message-outbox, useClubSetupMutations and other index exports; FeedList footer/listRef and feed-list-web.test.tsx (lane B); the DM-image and reviewed-repository-inventory expectations in two queue-integrity script tests.
    • Offline interruption is not a critical report (2026-10-06): a same-account QueueableControlError('offline') runs the mutation callbacks (rollback + reconciliation) but the global MutationCache.onError records only an offline interruption breadcrumb, never captureCriticalError — going offline mid-request is expected and would flood client_errors/Sentry.
  • CODEX LANDING F (2026-10-06): clubs repairs from Codex's PR #15 landed by area. An archived club is restorable by its owner (settings recovery view, archived rows in 내 클럽), transfer recipients with the same or no nickname stay distinguishable, and club creation persists its intent and keeps partial optional setup recoverable (ClubSetupReceipt, fresh paged reads before any retry write). Target-bound invite join stays out until migration 00686 (lane D3).

    • Mutation hook: useClubSetupMutations was a hand-rolled useMutation lifecycle beside the canonical one. It now sits on useQueueableMutation; the only canonical extension is an optional owner lifetime (getLifetime -> createQueueableCall) so a departed wizard stops dispatching. Same-club FIFO sequencing waits for the messaging lane's entity queue.
    • Archive window: the 30 days live once in CLUB_ARCHIVE_RECOVERY_DAYS (config/club-archive-policy.ts); copy and the restore deadline derive from it, and check:club-archive-window-parity compares it with the unarchive_club and purge_archived_clubs literals (canary test included).
  • CODEX LANDING STACK (2026-10-06): the integration branch codex-split/stack is the merge vehicle for the whole audit landing; merging it supersedes PRs #16-#24.

    • Merge order: d3 (carries d1/d2) <- a (CI/tooling) <- b (ui a11y) <- c (web) <- fix/script-test-timing <- f (clubs, carries e, after its offline-report fix). Generated docs were regenerated, never hand-merged; test:scripts, check:static, queue-integrity transport surfaces and decision-log bullets are unions.
    • 00690 (cron pre-locks): private.auto_advance_session_statuses() took the session tuple before the row trigger took the roster key, so it could deadlock (40P01) against an in-flight score. It now takes lock_session_roster(id, true) for every due session, in id order, before any status UPDATE (00681 order). The withheld cron case of the competition probe is re-enabled; pgTAP 00690_cron_prelocks asserts via pg_locks that the key is held when each UPDATE reaches its tuple.
    • Un-skips: the three tests d3 withheld run again (entity-ordered turns and offline rollback via f; guest names from the live composer via Codex d48e9e7a6, with the payments screen kept on usePaymentActions). WebSentryOwner binds Next's client (initSentry(NextSentry)), with its test. check-no-skipped-tests.test.mjs fails on any .skip, .only, xit or fit in the tree (canary included); feed-list-web.test.tsx joins now that b's FeedList props exist.
    • Found on the way (pre-push): guard-ota-release.test.mjs ran its fixture git add . with the hook's inherited GIT_INDEX_FILE, so the pre-push gate rewrote the real index (5,000 staged deletions) and failed check:screen-blueprints. The fixture now strips every GIT_* variable.
  • DOCS AND WAIVERS SWEEP (2026-10-06) · Stale status docs reconciled (roadmap, implementation plan, external blockers, club-surfaces build status, open-issues §5 answers, device List 52 for OTA 1719d507; List 47 install smoke stays the open owner item).

    • no-padding-top-in-features: 57 disables to 17. Repeated justifications are now encoded: first child of DetailShell/PlaceShell/WizardShell/ModalPanel (through pass-through wrappers), list-header slot roots, content after a Divider, SEAM.chrome, and cond ? 0 : X edge-rule drops. Second children and anything under a spacing owner still fail (canary tests). The 17 left are genuine one-offs (seam overhangs, safe-area clearance, wizard step insets).
    • DATA-3 enforced: @twomore/no-empty-catch-in-adapter (an empty catch needs a comment saying why; 8 adapter sites already have one). New DATA-26 row (write serialization, pgTAP). AGENTS.md trimmed to 661 bytes under the cap.
    • Waivers: knip 50 to 50 (all still fire, all already carry reasons), near-duplicates 213 to 212, duplicate-values 161 to 161, clones 971 to 946 duplicated lines.
  • PRODUCT BOARD (2026-10-06): /wireframes/product-open draws three open product questions as recommended final states.

    • P1 (홈 desktop summary pane): build from existing loads only (참석, 입금 대기 via sessionPaymentGateState, 치른 경기 from the recap); cut 예정 and 예비, which need a new RPC or read.
    • P2 (친구 참석 on 번개 찾기): count-only SECURITY DEFINER friends_attending_count(session_ids uuid[]), no identities; the alternative is no chip on pickups. Needs the owner's privacy call.
    • P3 (feed photo): the 72×56 thumbnail was already replaced by FeedPhotoStrip (2026-09-27); keep 4:3 for a single photo rather than a 16:9 band (a band saves 67px per post but crops a third of the photo).
  • VISUAL TONE AUDIT (2026-10-06): the owner's "pale overtone" on the web renders is measured in visual-tone-audit-2026-10-06.md. The phone app shares the cause (same pixels: card/canvas 1.04:1, every light surface inside L* 96–100, mint chips as content, 49% muted ink with cardMeta heavier than cardBody); the web amplified it — Tamagui text never rendered Pretendard on the web (family-name mismatch, fixed 3f2ce2e7 with a contract test) and the frame painted a second near-white (unified). Text ink is not the problem (7.4:1 / 5.5:1, darker than Toss). Token and grammar changes go through /wireframes/visual-tone (T1 surface ladder, T2 ink hierarchy, T3 chips→inline facts, T4 one accent, T5 stroke glyphs) for the owner's iteration before any app code; a canvas/card ΔL* gate ships with T1.

  • VISUAL TONE BOARD (2026-10-06): /wireframes/visual-tone answers the "pale overtone" with five before/after questions plus a 1440 frame; proposal only, nothing built.

    • Finding: text is not pale (every ink tier passes 5:1); surfaces are: canvas and card 1.7 L* apart, fills 1.09 to 1.18:1 on the card, chips holding plain facts, no content-level saturated anchor.
    • T1 ladder: canvas #EEF2EF (L* 95.1), new $surfaceStrong #E5EBE7, hairline #CDD6CF; card/canvas 1.04 to 1.13:1 (not 1.17), L* step 1.7 to 4.9; gate: step at least 3.5 and fills on card at least 1.2.
    • T2 to T5: promote the time (600), cardBody 500 and cardMeta 400; facts inline and badges for status only (saturated tier); day-count $primary anchor; lucide strokes at $text. T6 web fixes (font family, --tm-bg, hover) noted, nothing to decide.
    • Staged: S1 tokens+gate, S2 ink, S3 chips, S4 glyphs, S5 anchors; all JS/token-only, OTA-able.
  • VISUAL BENCHMARK (2026-10-08): the owner rejected T1's grey canvas ("modern Korean apps opt for flat, more pronounced backgrounds") and asked for research. Three lanes measured 60+ apps and the published token files (visual-benchmark-2026-10-08.md). The owner was right about #EEF2EF — greyer than every measured ground (Korean greys L* 96.5–97.8, Toss/Seed 96.1, iOS 95.6, category floor 95.8) — withdrawn. "Pronounced" turned out to mean one bounded colour block per hub screen (배민 26%, 카카오맵 22%, Monzo 18%) and ~5× our ink, not a darker canvas: 12 of 17 Korean apps are flat white on lists and no measured app separates cards with shadows. New direction "white, with one block": basement #F0F7F3 (ΔL* 3.4, the tone-only floor) on hub screens only, pure white + hairlines on lists/details, one solid $primary block per hub, optic #C6E84B as the racket category's level-chip accent (8 of 13 racket apps), promoted fact per card, photo-led rows, full-pill chips. Board redraw queued; nothing built until the owner marks it.

  • COLOUR BUDGET (2026-10-09): the owner's second hypothesis — "we use too much primary everywhere and not enough colour on content" — tested in lane D of the benchmark. Right on hue and role, wrong on quantity: our saturated area is below peers (1.4% vs 5.1%) but 96% of it is one green (peers 50–61%), 66% of 307 brand-token sites are control chrome where 12 of 17 peers use ink/neutral, mint fills stand where peers use grey, and the content palette (optic, tier hues, club identity hues) has no consumers. Budget adopted as benchmark §5: brand = one action + one block + links; controls = ink/neutral; semantic = status; optic = level; content accents small and multi. Board round 3 draws it (T7) and neutralises the chrome on every recommended phone. Side bug queued: dues-attest-modal.tsx white on mint.

  • COLOUR SYSTEM RESEARCH (2026-10-09): the owner approved "white, with one block" + the colour budget and asked for every proposed change to stand on colour science and UX research. Three lanes (perception, UX practice, palette engineering) plus one shared computation helper (CIELAB/OKLCH, ΔE2000, WCAG, APCA with the published size LUT, Machado CVD) produced the ledger in colour-system-research-2026-10-09.md. Direction holds; 11 values changed: control track = $borderSubtle #DFE7E1 (fills must pass ≥ 1.15:1 on the basement too), hairline = $border #CAD4CC, meta 13/500 secondary (13/400 Hangul stems render at 0.85 device px), tertiary only ≥ 16px, block labels ≥ 14/700, success → teal #00796A/#014B40 (today ΔE 6.7 from brand under every vision type), warning → #6B4A00 text + #F5B301 fill with ink (today error/warning collapse to ΔE 4.2 under deutan), dark error #FA9B94, court trio and tier hues recoloured (grass = brand ΔE 2.3, clay = error 6.6, gold 2.38:1), $series1–5 added, dark ink #DFE4DC, dark block #015228; badges stay rounded rectangles (Seed's badge is 4–6px — the earlier "full pills" misread it), icon stroke 2.0, inline links underlined (brand vs ink 2.95:1), controls in ink with a non-colour second cue. Dropped as folklore: 60-30-10, the 49%→30% muted-ink share, the proportion targets as rules. Gate grows to nine assertions incl. a CVD-separation check with canaries that fail today. Five owner decisions (D1 success teal vs 모집 중, D2 computed hexes need eyes, D3 court colours, D4 dark block, D5 tertiary sweep). Board round 4 applies it; nothing built.

  • OWNER RULINGS ON THE VISUAL BOARD (2026-10-09, after the research pass): (1) peers keep buttons, actions and navigation neutral; colour marks the single most important action per screen; photos carry identity; inline icons are minimised, icons are action targets (top-right, bottom nav). (2) If 홈 leads with a brand block, the list items below must be subtle: move list surfaces to the FLAT language of the club detail pages (toned-down navigation and items) and make T1 pure white — no basement. The brand block itself must be redesigned (alignment, actions, distribution). (3) Ink hierarchy is top-down: the first line carries the most weight; the mid-card promotion is rejected; text hierarchy work continues. (4) Chips and badges are the same component once chips stop being navigation: unify design and implementation (one passive Tag; wizard multi-select options stay form controls). (5) T4 accents: decide what deserves attention; the session card has too many attention seekers — drop the bottom row and compact it. (6) Colour roles and the semantic palette: yes, configure properly. (7) T5: the alternative (glyph tier in the hero, quiet list rows) is preferred; research emoji vs glyphs on dense lists. (8) Web after mobile components. Three research lanes (flat lists under a block + block anatomy; tag unification + top-down row hierarchy + session-card attention inventory; emoji/glyph tiers) run before board round 5.

  • LIST SURFACES SPEC (2026-10-09): the owner's rulings measured into list-surfaces-spec-2026-10-09.md (lanes H/I/J). Decisions taken there, each with the evidence: the brand block is pressable content with a chevron and holds no button (one coloured action per screen = the FAB; Toss precedent; 7 of 9 peer blocks do hold a button, so this is a budget choice); day-grouped lists lead with the time (3 of 4 grouped peers; venues repeat, times do not); rows are spacing-separated with no divider (two-line peer median 62.6pt; our club-detail canon 64 is inside the band); one trailing element per row, never count + chevron; 48/72 tiles for session rows rather than a new avatar rung; neutral weak Tag fill at gray100 L* 96 (every peer row pill), control track $borderSubtle; underline tabs replace the segmented pill on list surfaces; the basement is withdrawn because flat rows need no card step; glyph vocabulary 21 → 4 concrete, hero tier only, weather the one emoji exception. Owner's: D6 bookmark leaves the row (overturns 2026-09-08), D7 live ribbon → trailing danger Tag on compact lists (touches the 2026-09-08 carve-out). Board round 5 draws it; web waits for the mobile grammar.

  • FLAT GRAMMAR V2 — FROM SCRATCH (2026-10-09): the owner judged round 5's recommended phones "quite ugly": patching flat rows into the old card grammar reads as a patch. Ruling: if we go flat, reconfigure every component on the page and work out their ratios; design from scratch with best practices. flat-mobile-grammar-v2-2026-10-09.md sets the system — 390 × 844 reference, 4pt grid, rail 20, type 12/13/15/17/20/24/40 anchored on body 15 (Apple subhead / Toss T6), seam ladder 8 : 16 : 32, media rows 80 (thumb 56) and person rows 64 (avatar 40), block 144 (17% of the viewport, padding = rail), tag 24 / choice 36 / button 52 / FAB 48 / tab bar 56 + 34, radii 6/8/12/16 — and six screens composed only from it. A new board /wireframes/flat-v2 draws it at 1:1 with DOM-measured verification; /wireframes/visual-tone stays as the research record.

  • PRIMARY COLOUR QUESTION (2026-10-09): with the flat grammar accepted as "much cleaner", the owner asked for colour schemes because the dark green primary "drags the whole thing down". Measured: #006D37 is L* 40 / C* 46 while Korean primaries sit at L* 55–90 / C* 67–90; a green that keeps white text at 4.5:1 cannot pass L* ≈ 48, and a fresh green is ΔE 4–6 from Naver/Band. Four schemes in colour-scheme-proposals-2026-10-09.md: S1 lifted green #0F8446 (minimum change, does not fix the weight), S2 Ball & Ink (ball #D6F04A actions with ink text, ink block with a ball numeral; green survives as the mark and court tile), S3 Hard-Court Blue #2E5BE6 + ball (Playtomic pairing; 총무 trust), S4 Clay #C84F27 + sand (collides with error semantics and 당근). Recommendation S2, alternative S3; rendered on /wireframes/flat-v2 §G as 홈 + 세션 상세 per scheme.

  • SEAMS ON FLAT SURFACES (2026-10-09): with S2 provisional (S1 backup), the owner asked whether flat sections get dividers. Lane K (22 peer screens, Apple/Material/Seed/Toss/GOV.UK, Gestalt sources) → grammar §4b: rows no line (9 of 17 peers spacing-only); single-line 48 cells get a quiet inset hairline #E5E5E5 1.26:1 (D8, amends the FactRow rule); sections divide by whitespace + title at 32 ink-to-ink, no line (none of 22 screens draws one; retires the 2026-09-06 section hairline — D9 — whose failure mode, section edges reading like rows, cannot occur once rows carry no line and open with a 17/600 title); one 12pt #F2F2F2 band with 24/24 for a change of kind, at most one per screen (D10); block → content 24; tabs → content 24; sticky edges draw #D4D4D4 only while content passes under (Apple scrollEdgeAppearance, Material liftOnScroll); no shadows; dark: α0.14 / α0.09 lines, band darker than canvas. Board /wireframes/flat-v2 switches to S2 and adds §H with the three devices side by side.

  • MIGRATION PROGRAM (2026-10-09): the owner accepted S2 (S1 backup), the seam rules (D8–D10) and the recommendations, and set the order: foundations first, then every shared component designed with all interactive behaviours (one library used throughout), then page layouts composed only from that library, then build. flat-grammar-v2-migration-plan.md: Phase A foundations (tokens + gates), B component library board /wireframes/flat-v2-components, C page-template boards, D build S1–S8. Nothing adapted from the old grammar; no mixed screens. Phase B begins with the component inventory.

  • COMPONENT LIBRARY SPEC + FIFTEEN RULINGS (2026-10-09): Phase B. The inventory (255 exports; 123 keep / 74 re-spec / 34 merge into 7 / 24 retire, 17 of them dead) found fifteen conflicts between the grammar, the list spec, the canon and the code; flat-v2-component-library-2026-10-09.md §0 rules on all of them: Tag 24/r6/13·600 (md 28, level 24 circle, count 20); Choice 36 (44 in forms); block per the grammar (list spec §3 superseded); rows 80/64/48 (list spec 72/96 superseded); underline tabs are content-width left-aligned (group-spread law narrowed to strips); Divider law amended per §4b; no middle dots (MetaBreak); FAB allowed, one per list screen (canon "no FABs" retired); tab bar ink; rail 20; no shadows (toast ink surface, menu edge hairline); $primarySubtle leaves with per-component replacement cues; avatars 40/48/56; code CellRow → Strip, new Cell 48; controls ink, focus ring ink, ball only on the primary action. Behaviours carried from code with values (press springs, dims — row dim .70 → .80 — hover .85, hit slop, toast 2800, sheet 200 ms, skeleton .6↔.9) plus two new ones (edge hairline on scroll with 8px hysteresis; FAB collapse after 120px). Two boards render it: /wireframes/flat-v2-components (chrome, surfaces, rows, labels) and /wireframes/flat-v2-controls (controls, overlays, states, forms), each with a state row per component and DOM-measured heights.

  • COMPONENT LIBRARY BOARDS (2026-10-09): /wireframes/flat-v2-components (chrome · surfaces · rows · labels; 46 components, 708 measured roots, 0 off spec) and /wireframes/flat-v2-controls (controls · overlays · states · forms; 104 specimens, 514 text nodes on the scale, 0 off) — every component at 1:1 with anatomy rulers, a state row (rest / pressed with the dim and scale applied / hover / focus / disabled / loading / selected / error), variants, the behaviour note and what it replaces; dark strips on both. Values the boards derived where the spec was silent, for the owner: header back + title (title at x 56, L1); today dot inline vs hung (L2); block focus ring inside the ink (L3); Notice and offline strip held at 48 (L4); dark weak tints at 16% over the canvas (L5); toast 16 above the tab bar (K1, code uses 8); unchecked checkbox 2px #4F5851 edge (K2); a 44 medium Button size (K3); derived dark tonal/switch/backdrop/toast values (K4); the code's toast variant glyph kept (K5); ghost pad 0 12 and the wizard progress line on the header's last 2pt (K6). Library row dim .70 → .80 stands; the block keeps the card physics .98/.90.

  • LIBRARY VALUES RESOLVED + PLAN GAPS (2026-10-09): owner: "go with best practices and your recommendation. Is this everything we need to change our UI/UX?" Resolved L1–L5 / K1–K6 in library spec §13 (pushed-screen title 17/24 at x 56 with a scroll-linked title on hero pages; hung today dot; one ink focus ring incl. the block; Notice 48; toast 16 above tab bar/FAB; 2px unchecked checkbox edge; Button sm/md/lg for every tone; toast glyphs in dark tones on ink; wizard progress line in ink, not ball; block press .98/.90). Answer to the question: no — the plan gained B6 text scaling (the app does no font-scale handling), B7 motion, B8 a11y semantics, B9 platform, C0 IA + task flows (the usability audit's tap counts are the baseline), every-state page boards (incl. English and 200% text), A2 brand touchpoints (icon/notification/splash are still green and native), D0 canon/lint/test rewrite, D1 dev-panel component gallery, strangler migration with codemods, and a 5-person validation after the first template.

  • C0 RULINGS + ELEVEN NEW COMPONENTS (2026-10-09): the IA audit (flat-v2-ia-and-flows-2026-10-09.md) mapped 72 screens (hub 4 · list 28 · detail 22 · form 12 · settings 6), corrected the usability baseline (bulk dues, 납부 취소, 관리 and 초대 코드 entries already shipped) and found bugs. Its 18 decisions ruled in §6 by best practice, adopting the recommendations except dues reconcile, where only 확인 대기 rows are pre-ticked (pre-ticking 미납 invites false "paid" records); sign-up stays EXT-11 (owner/external); default-first session creation logged for veto. Bugs fixed now in a code lane: payer payment pushes dead-ending at a permission wall, the 3-hop join-request push, unsafe back on cold-start deep links (Expo Router anchor + appRouter.back() fallback), onboarding dropping ?code=, signed-out web join no-op, dead route builders and /courts. Library §14 adds the eleven components the pages need: HeaderTextAction, ActionSheet (> 5 actions), CTA band helper line, MediaBand (photo with an overlaid header that turns white on scroll; map), PostRow, MessageBubble + Composer (mine = ink; send = the ball action), Calendar (month + week strip; 일 danger, 토 series1), Podium, ScoreEntry, LiveScore, Select mode, CountdownNotice. Brand: the owner will recreate the mark — the brand lane builds touchpoints around a mark slot plus a mark brief.

  • BEHAVIOUR ADDENDUM RULED (2026-10-09): flat-v2-behaviour-addendum-2026-10-09.md — B6 text scaling (follow the OS 85–200%, caps by role, stack from 1.5×, every text-holding height becomes a minHeight), B7 motion (timing tokens 100/150/200/250/300 on Material curves; no layout animation in virtualised lists), B8 spoken-label patterns and one h1 per screen, B9 platform (predictive back stays off; iOS 26 full-screen swipe disabled on slider and wizard screens; native tabs not adopted). Q1–Q10 ruled by its recommendations: body cap 2.0, stack pivot 1.5, iOS sheet swipe-down, no ELO count-up, toast 4/6/8 s with ×, fullScreenGestureEnabled: false, Bold Text mapping later, PersonRow pitch 64 → 68 (the text block is taller than the avatar), 되돌리기 as the undo label; the 3-button-nav seam fix waits for the native batch. Library §15 carries the amendments.

  • BRAND, PATTERNS, NAVIGATION FIXES (2026-10-09): brand touchpoints built around a mark slot (the owner will recreate the mark; flat-v2-brand-touchpoints-2026-10-09.md): icon field ink #15181A recommended (ball mark 13.95:1; nearest peer tile ΔE 37 vs a ball field 4.5 from 테니스프렌즈), one 66/108 safe circle for every touchpoint, notification accent #02914C (4.07 / 4.05:1 on light and dark shades, the single-colour ceiling), splash white / #0F1211, theme-color pair, OG cards in the grammar, a mark brief R1–R10 with four concept directions; current faults recorded (the adaptive icon is 98.7% #006633, no monochrome layer, white splash in dark mode). Patterns board /wireframes/flat-v2-patterns draws library §14, B6 (100/130/150/200%), B7 (24 motions), B8 (focus order + spoken labels), B9 (insets); P1–P5 ruled in library §16. Navigation fixes shipped with tests (payer pushes, one-hop join review, root anchor + back fallback, invite code through onboarding, signed-out web join → sign-in, /courts and dead builders removed); the Phase C board contract written (flat-v2-page-board-contract-2026-10-09.md).

  • PAGE BOARDS WAVE 1 + LIBRARY §17–§18 (2026-10-10): flat-v2-hub (91 frames, 4 hub routes incl. the club hub's six panes) and flat-v2-list-a (183 frames, 17 list routes) drawn from the library only, every contract check passing (list-a's own checks caught seven real defects before handback). Rulings: H1 header order (text action left of icons, overflow ⋯ always rightmost), H2 홈 Block → 경기 › 내 경기, H3 recap leaves 홈, H4 club pane label 순위, H5 month pickers = Choice → Sheet of every month; D1 notifications dismiss via Select mode (SwipeAction only as an accelerator) with a hung unread dot, D2 request rows get an inline tonal answer pair, D3 header subtitle for context, D4 row ⋯ always an ActionSheet, D5 앨범 3-up full-bleed grid, D6 terminology guide governs 참가비 words, D7 신고 관리 persistent in the 관리 sheet, D8 unresolved attendance rows get an explicit 참석/불참 pair (a bare tap never records). Library §18 specifies the twenty components the boards needed (unread marker, SwipeAction, AnswerPair, PhotoGrid, SectionHead variants, Tabs count, FilterSheet, Edit Sheet footer, danger CTA band, PersonRow trailing meta, caption row, activity heatmap, MediaBand album overlay, PostRow system variant, club hero place variant, header icon badge, task Cell, two-line Cell, reaction picker).

  • DETAIL BOARD RULED + LIBRARY §19 (2026-10-10): flat-v2-detail (272 frames, the 22 detail routes, 79,564 DOM checks, 0 failures) drawn from the library only. D1 host/official session header [관리][⋯] with 일정 수정 as the first 관리 row. This amends EDIT-PENCIL's header placement on the owner's own ground ("back to two glyphs with ⋯"); the in-row pencils stay. D2 참석 취소 = lg tonal, danger only in its Dialog. D3 ruled against the board's proposal: the map band stays for every session whose venue has a map. The fold test is "head + first row above the CTA band", because a cut second row is the scroll cue (NN/g illusion of completeness) and a band that depends on roster size would give two page structures. D4 ScoreEntry in a Sheet with 되돌리기. D5 profile header [메시지][⋯], 공유 in ⋯. D6 library first. Three library lines contradicted shipped owner rulings and were corrected (§19.2): the map band takes the photo geometry (MAP-BAR-INK), no "지도" Tag (MAP-NO-CHIP), and tap opens 장소 상세, not the map app (MAP-NO-CHIP). The two-line Cell is 64 → 68 (PersonRow's text block). §19.3 specifies the 18 gaps: ExpandToggle lone row with a counted label and a disclosure Cell (chevron down/up, independent opening), reason and Tag-value Cells, roster door, hero fact count + one viewer-state Tag, row-centred trailing controls, composer reply target, group avatar (two photos, never a glyph), anonymous avatar, Comment (indented body, one reply level), Live row, Strip deltas (plain signs, no red/green), a Form strip on head-to-head only (never dots in rows: WCAG 1.4.1), ScoreEntry Sheet keyboard rule, ActionSheet as the generic object sheet, Reactions (quick row + summary chips; no centred panel). Found: three reaction vocabularies (match 5, post 5, DM 6). The recommendation is one set (👍 ❤️ 👏 🔥 😂 🎾) in one domain registry, migrated while the database is not live.

  • LIST-B BOARD RULED + LIBRARY §20 (2026-10-10): flat-v2-list-b (125 frames, 친구 · 업적 · 연습/경기 기록 · 리더보드 · 차단 · 동의 이력 · the 테니스장 pane · receipts; 0 FAIL across 1,226 stack items, 2,576 text nodes and 250 targets). Rulings:

    • L1: a 친구 찾기 row opens the profile (PERSON-ROW canon; today's static row is out of canon).
    • L2: the new copy is approved, subject to the gates.
    • L3: the 테니스장 kind chips move into the FilterSheet; 저장한 코트 becomes the leading section.
    • L4: BADGE-EXCEPTION is kept (owner ruling); library §5's "every rank row" is amended.
    • L5: 정렬 is split from the filters, as a Choice ▾ that opens a Sheet.
    • L6: the unreachable 업적 states stay drawn and marked; the build schedules the unlock engine or deletes them.
    • G: six gaps were already specified; four are new in §20.2 (Text row, glyph-tile Thumb, row trailing control, figure column + rank width, permission EmptyState).
  • FORMS BOARD RULED + LIBRARY §21 (2026-10-10): flat-v2-forms (276 frames, 70 scroll captures; the 12 form/wizard routes and the 6 settings routes, with IA ruling 9's default-first 일정 만들기). Rulings:

    • A1 no separate step-by-step entry: a first creation, or one with nothing to copy, gets the steps automatically.
    • B1 the quick path drops the 지역 screen when the profile has a region (6 taps).
    • C1 the leave guard goes on every form, with create and edit copy.
    • D1 every Choice ▾ opens a Sheet; the title stays 공개 범위 설정.
    • E1 the routed 관리 page is retired as a menu. The 관리 ActionSheet is the one list, attention counts sit on its Cells, and /admin becomes an alias.
    • F1 library §21.2.

    Two board departures were reversed because they broke owner LAWS:

    • 건너뛰기 drawn as ghost text: the wizard-navigation law requires real buttons, so §21.2 adds the WizardShell header (40 gray100 square back, sm tonal 건너뛰기).
    • Required fields drawn without the asterisk: the required-marker law keeps the one LabeledField required asterisk.

    Also: every editable field keeps its Cell on the review page (경기 유형 was hidden at its default). Save confirmations are Toasts.

  • AUTH BOARD RULED + LIBRARY §22 (2026-10-10): flat-v2-auth drew 85 frames with 0 FAIL across 11,495 checks: sign-in, onboarding A1–A4, consent, the version gate, the app receivers and the web share pages. The sign-up route stays the owner's open question (EXT-11), with Kakao drawn as the target and the email variant beside it.

    • Order reversed for law: the shipped wizard collects the birth year first so the under-14 gate precedes consent (PIPA §22-2). The name follows consent. The new-user path is therefore A1 → 출생 연도 → 약관 동의 → 이름 → 첫 시작, 6 taps (12 today; IA's 5 assumed consent first).
    • Value props, welcome and done are retired. The consent prefix stays the shipped [필수]/[선택].
    • Onboarding sign-out clears the draft.
    • Gap recorded: there is no password reset for email accounts.
    • §22.2 specifies the Kakao ProviderButton (mandated values, system font, radius 6, the one coloured action), the CTA band entry variant and danger helper, the brand lockup slot, the consent Cell (전체 동의 with a mixed state, nothing pre-ticked), the busy Cell, the document Sheet, the full-screen EmptyState, the header-less offline strip, and the web install row (blocked on EXT-8).
  • ONE REACTION SET (2026-10-10): the code holds three reaction vocabularies: match (thumbs_up, fire, muscle, clap, heart), post (like, clap, fire, heart, laugh) and DM (👍 ❤️ 😂 😮 😢 🎾). Ruled: one registry and one set for every surface, 👍 ❤️ 👏 🔥 😂 🎾. The build migrates the two DB enums while the database is not live. The hub and detail boards draw it.

  • CONSISTENCY PASS (2026-10-10): applied across the hub, list-b and detail boards:

    • in-place expansion uses the ExpandToggle lone row;
    • 친구 목록 request rows open the profile (PERSON-ROW);
    • 묶기 moved from the filters into the 정렬 Sheet;
    • earned achievement tiles take the §20.3 medal set;
    • the Disclosure Cell pad returns to 16 (text-driven heights are exemptions).

    Process lesson: the pass deleted part of an untracked board mid-edit and restored it from the authoring agent's transcript. Finished boards are now committed locally as soon as they settle.

  • FLOWS BOARD + HOST CTA BY STATE (2026-10-10): flat-v2-flows composes the nine task flows from the page boards' own phones: 89 tap markers on 121 phones, 560 checks, 0 FAIL. Measured targets are in IA §3; sign-up is 6, session-from-blank 10 and dues against a bank statement 7, each one above the IA target because a later ruling or a recount added a tap.

    • Ruled: after 마감, the host's CTA band follows play state. No draw shows 대진 짜기 (it opens the 경기 보드 and its Sheet); a ready draw shows 경기 시작; play in progress shows 경기 보드. The detail board had a disabled 경기 시작 with no door to the board.
    • The 관리 sheet's 참가비 관리 row gets its Task Cell count Tag.
  • ADDITIONS BOARD + LIBRARY §23 (2026-10-10): flat-v2-additions draws every component the page boards added (§18.2–§22.2: 55 specimens, 566 frames, 0 FAIL, 31 conflict rows with fixes). The patterns board was corrected (map band on the photo geometry, text action left of icons). The controls board was corrected to the owner laws (wizard header, required asterisk). Rulings:

    • The ScoreEntry Sheet opens without the keyboard, quick picks first (all seven kept); typing raises the keypad and folds the quick row. This keeps the sheet under the 88% cap.
    • Dark mode gets a tertiary text token at the light ratio.
    • The search field is 40 r12 inside a 44 target.
  • OWNER REVIEW 1 RULED (2026-10-11): the owner reviewed the library boards (23 points) and asked for research, best judgment, and a cleanup of every board. Three research lanes informed the rulings (rulings; lanes A, B and C in research/).

    • No hanging markers. A dot takes an 8 + 8 slot inside the rail on every row of a list that can show it; the today dot is dropped.
    • Headings. 32 above / 12 below; day header 24 / 12; section titles 20/28·700.
    • Type. A 16/24 reading role; tab labels 12; nothing below 12 in Hangul; labels scale to 200%.
    • Rail. 20 kept with evidence: Apple's own margin from 390 pt; 16 below 390, 24 from 600.
    • Blocks and strips. One block grammar (eyebrow → figure → meta, whole-block press, one state table). Strip at equal pitch, label above figure, no hairline.
    • Notice v3. Title first, no generic icon, five tiers.
    • Session rows. The venue leads; the time leads only where every row shares the court.
    • Fields. One field grammar with a 3:1 border, helper text rarely, validate on leave; $errorText #A3261F.
    • Wizard. The split is kept; progress gets its own row, 16 clear, with a count.
    • Header. Ten icons; text only for mode controls, drawn as sm buttons. 관리 becomes a gear with a badge plus body entries, with a five-official test and a labelled fallback.
    • Overlays and photo bands. One overlay rule; ActionSheet renamed MenuSheet. Photo bands carry ‹ + at most 2 icons; venue actions move into a labelled row.
    • Messaging spacing tuned.
    • Live score. A full-screen recorder counting games, with labelled undo.
    • CTA helper and copy. The helper carries decision facts only; the primary is soft-disabled. Toasts carry results and undo only; errors stay.
    • English and accessibility. English line budgets; Reduce Motion becomes a fade; 48 targets, FAB 56, Switch 52 × 32.

    Answered against the suggestion, with evidence:

    • the wizard split kept;
    • the roster becomes one row of four, not a horizontal scroll (0 of 8 peers scroll people);
    • the 20 rail kept;
    • live score counts games, not points.

    Reversed my own earlier choice: the edit sheet gets × in the title and one 저장 (KRDS, Seed, HIG), replacing the no-× 취소/저장 pair.

  • LIBRARY v2.1 CONSOLIDATED (2026-10-11): the layered library (§0–§23) and the owner-review rulings are folded into one specification: 11 families and about 100 components, each entry once, with current values only. The layered edition is archived in docs/history/. Open items settled while merging:

    • Timing. Skeleton: nothing for 300 ms, then held at least 500 ms; cached data replaces a skeleton.
    • Chrome. Tabs 48 tall with the 2pt underline kept; the badge dot is 6.
    • CTA band. The helper is one 15/22 style, with a danger variant in $errorText. A button pair appears only in sheet footers, never in wizards.
    • Wizard. Segments are tappable up to 7; beyond that a plain bar.
    • Errors. Background failures become a Notice, never a toast.
    • Media. The album counter is passive. Every bar riding the keyboard drops the home inset.
    • Dark mode. gray100 #232824; the block gets its own #2A302B.
    • Rail. 20 between 441 and 599 pt.
    • Roster. Two rows of two from 1.5×.
    • ELO block. 160 tall with an 8 bar.
    • Notice. Title 15/22·600 over a 16/24 body.
    • LiveScore glance. The serve is shown as the ball glyph plus 서브, the same mark as the recorder. The block opens the recorder for the host and officials, and the 경기 보드 for participants.
    • Level tag. Capped at 1.5, as a numeral.
    • Week strip. Prev and next buttons (WCAG 2.5.7).
    • Presence dot. Success colour.
    • Message bubbles. 16/24, matching the composer.
    • Choice set. At most 5 options, or 7 for a fixed series.
    • Live recorder. For the host and officials; participants use ScoreEntry.
    • Question-title MenuSheet. Closes from its × only.
    • Time range wheel. 24-hour.
    • Send button. Soft-disabled gray100 while empty.
    • Required legend. Shown on forms with six or more fields, or that mix required and optional.
  • 대진표, NOT 경기 보드 (2026-10-11): the match board page is 대진표. That is the shipped common.matchBoard string, and the terminology guide keeps one word per concept. The design docs had used "경기 보드"; the boards, the library, the IA doc and the rulings now say 대진표. The host's CTA after play starts reads 대진표.

  • Page-board consistency rulings (2026-10-11, logged for veto):

    • 알림 filter: one Choice ▾. Seven filter values that come and go with the data are more than a Choice set holds (five; seven only for a fixed series), so the row of chips becomes one Choice ▾ showing the current value ("전체 ▾"). It opens a value Sheet whose rows carry their counts in a figure column.
    • 대진 짜기 lands on its Sheet. The session's 대진 짜기 opens the 대진표 with the method Sheet already up: one intent, one tap. Flow 6 is 5 taps (from 6).
    • ScoreEntry has its own door. The live Block opens the recorder. A finished match that was not counted live carries a 점수 입력 sm tonal button in its row, which opens ScoreEntry for that match. Flow 7 is 3 taps for the host. A participant agrees in 2.
    • Leaderboard 기간. A period lists the players who played a rated match in it, still ranked by current ELO, because ELO is the one truth (ratings calibration). "Most improved" (the ELO gained in a period) would be a separate ranking, not a filter.
    • A break never starts a line. FvkMeta wraps whole facts. Each fact's bar sits in its own leading padding, and the row is clipped, so a fact that starts a line shows no bar. Section-head counts move under the title as one piece at large text sizes.
    • One drawing per pane. The 테니스장 directory pane is drawn once, on List B, and List A points to it.
  • Leftover proposals folded in (2026-10-11, logged for veto):

    • 친구 참석 on 번개 찾기 (product-open P2): yes, counts only. A discovery row carries "친구 N명" from a count-only RPC; names stay off the row (privacy), and the count is social proof where it helps a join decision. List A draws it. P1 (the 홈 desktop summary pane) waits for the web track; P3 was settled by the flat v2 PostRow. The board is marked decided.
    • Guest review flow: superseded by List A's 멤버 관리. The review queue (신청 and 지명 rows), 게스트 지명 and the role groups are drawn there; the board's three backend gaps (a club-wide 심사함 read model, nominate and vouch signals, guest-fee settlement) move to that page's spec as Needs.
  • Owner review 2 — marks and rulings (2026-10-11): the owner approved foundations, library chrome, overlays and behaviour; approved surfaces and rows with changes; asked one question on controls; and asked for a ground-up rework of score recording. Applied (logged for veto):

    • One NTRP per person. The ELO block's eyebrow shows the person's NTRP ("ELO | NTRP 3.5"), the Level tag's value, never the tier's band; bands stay in the tier explainer.
    • The identity block is one button with one chevron, like every block; it opens the public profile, and 프로필 수정 lives in 설정 › 계정 (EDIT-PENCIL covers rows that edit in place, not a door).
    • Strip end spacing. In a row of figures every column but the last shares the width and the last hugs its content: the starts stay evenly spaced (the owner's review-1 rule) and nothing trails the last figure. The fit test follows: every other column fits its figure and its label's longest word in the pitch − 8; a last column over 40% of the content reflows two-up.
    • Form strip summary. The cells end with the count ("3승 2패"), which is also the spoken label.
    • PersonRow line 2 is optional and never repeats a fact the row shows: with a Level tag, the NTRP line goes (the kit drops it); ranked lists carry no NTRP line.
    • Ranks start at the rail; the column is the widest rank's measured width on the 4 grid: 20 (to 99), 28 (to 999), 40 (to 9,999). Lane B estimated 16/24/32 at 8 per digit; Pretendard 600 tabular measures "99" 16.7 and "1,005" 36.9, so 16 would clip and 32 cannot hold the comma.
    • A doubles pair is never run together. A team alone on its line takes the 1px break ("박지훈 | 최유나": the result row, the Match state block, the sheets). Where two teams share a line (a match line) or a team sits inside a fact ("김민준, 이서연 승"), partners join with a comma and each name keeps its spaces unbroken: drawn with breaks, the English match line wrapped as "Seoyeon Lee vs Jihun Park", which reads as the match.
    • Guests are marked, members never: in a club session the roster tags 게스트 (a pickup tags nobody); in a list with a row control the guest is a line-2 role fact.
    • The consent group keeps one rhythm (no extra gap before marketing); the [필수]/[선택] prefixes and the order mark the optional ones.
    • Club hero: the band edge cuts the logo in half (28 of 56; LinkedIn 51%, X 50%, Strava 48%) inside a 2px canvas ring. No member faces in the hero (the owner asked whether it would be too noisy: yes; 5 of 7 observed heroes keep faces out, and faces belong in the 멤버 section for members). Flag, not changed: 00260_discovery_avatar_visibility.sql defaults avatar visibility to public for discovery viewers, looser than "non-members see no faces".
    • AnswerPair stays under line 2, now two equal columns, stacking from 1.5× text; never trailing (a trailing pair squeezes the name, and equal widths keep both answers the same size in both languages).
    • One score format: the hyphen ("6-4 6-3"), as the terminology guide already rules ("scores … keep the hyphen"). About 60 board scores used an en dash, the club-challenge tallies "2 : 1"; English records take the shipped recordValue form ("32W 15L", not "32–15"); Korean date ranges take "~". The library notes the shipped " : " strings (signals, clubs.scoreboard, tiebreakScoreline) and the 을/를 after a digit (josaEulReul falls back to 을: "6-4을") for the build.
    • The board verifier's dot and range checks had been passing blind: the in-page check was a template-literal string, which cooked \d and \s into d and s. It is now a real function with its text rules sent as JSON, each rule tested on known positives in Node and again inside every page, plus a new score check.
  • Score recording, rebuilt for retroactive entry with explicit consent (2026-10-11, owner review 2; reverses review-1 ruling 19, the live recorder). Basis: docs/design/research/owner-review-2-lane-a-2026-10-11.md (14 products; the shipped model at its latest migrations).

    • Players record after the match; the submitter's team counts as agreeing and one opposing player confirms; any player in the match may dispute while it is pending.
    • Silence never confirms (the owner's "explicit consent"): one reminder at 12 h; at 24 h the request also reaches the host, who confirms one row or all (모두 확정). Policy: autoConfirmHours = null. The research recommended Playtomic's 24 h auto-confirm; noted as the alternative.
    • A dispute keeps the claim with an optional counter; the submitter can take the counter in one tap; otherwise the host decides (version A, B, 직접 입력, 기록 제외); 7 days unresolved → 기록 제외.
    • Host override: only a host or official not playing that match; final, tagged 호스트 확정, every player told, one 48 h contest.
    • ELO moves only when a result is accepted. The live recorder retires; the LiveScore glance block becomes the Match state block (the viewer's next action, then the claim).
    • Eleven shipped defects in today's score flow (the consent push reads "대진 / 대진" and opens the host's board; the +10% bonus is unreachable; a rejection deletes the claim; and eight more, research §1.3) are fixed by the rebuild, not patched now.
  • Owner review 3 — library fixes (2026-10-11; rulings: docs/design/owner-review-3-rulings-2026-10-11.md; evidence: research lanes A, B, C of review 3). Applied (logged for veto):

    • One place for every rule: the block chevron is drawn once, centred, by the block frame; 52 custom implementations moved onto shared pieces (line slot, helper line, text action, badge host, equal-cell grid, one Cell shell with check and radio leads, one trailing builder, team lines, one match row, field box, one press table, one focus ring).
    • Inline adornments (the line slot, IA1 to IA6): a tag, count, dot or glyph beside text sits in a box as tall as the label's first line, centred in it; never a hand-set offset. A verifier check (adorn, ±0.5 px) guards it. All 52 tab counts were 2 px low.
    • Tab bar 64 + inset (icon 12 from the top): the badge gets 8.5 of headroom. A badge on at most two tabs; a count only where actionable.
    • Wizard header 12 and 12 (reverses review-1 ruling 10's "16 clear"). Bottom bars pad with the safe area itself, at least 16 (Toss's rule): 118 → 102.
    • A Tag, count or level numeral never owns a row. The Match state block's Tag ends its top line, and only when that line is not the viewer's action; the block is 144 in every state with derived lines. The create-session provenance row goes to the header subtitle; lone filter rows move into their section head.
    • Strip, final law (third ruling; reverses review 2's hugging last column): n equal cells, content centred, no dividers. The layout depends only on n and the text size.
    • Caption row: three roles (object line 8 above; section note 4 under the title, inside the head; page line 12 under the header), 13/20 secondary, at most two lines, and seven wording rules.
    • Form strip summary at the right margin (reverses review 2's "12 after the cells").
    • PersonRow line 2 from a 21-context table: at most two facts, never a repeat (the kit drops a fact equal to line 1), constants move to the head; 64 without line 2. The ranking context keeps RANKING-FOLD (2026-09-28): the record "N승 N패" and the 7-day change ▲/▼, omitted at 0; in 지역 and 전국 the club name replaces the change. The first review-3 table had dropped it; restored before deploy.
    • Choice sets never strand one chip: when a wrap would leave one Choice on the last row, the row before gives it one (4 + 1 becomes 3 + 2). A set that cannot spare one holds sentences, not labels, and becomes check rows: the manner-tag sheet is now one co-player at a time with the five tags as check rows (당근's 칭찬 list), instead of a chip set per player in one sheet. Tags and data are unchanged.
    • Sign-in busy state: only the tapped provider shows the spinner; the other buttons are disabled until it settles.
    • AnswerPair 12 / 36 / 12 (refines review 2).
    • Colour: the light danger fill becomes #A3261F (one red for fills and small text); the success solid becomes #00695C; the dark inverse surface flips to #DFE4DC with ink text (new key onInverse). Dialog caps at 560, Sheet at 640.
    • Kept as a recorded departure: skeletons from 300 ms with a 500 ms hold (Seed says 1 s); the button pair at one third and two thirds (HIG prefers equal sizes).
    • Score flow, one machine (lane C §3): countered and contested states added; the host hears of a counter only when it is declined or ignored for 24 h; a partner's counter is a new claim, never a confirmation; a contest goes to another official, or the host when there is none, and stands at 7 days; winners are emphasised only on accepted results; ratings apply after the 8 s undo; a correction needs one opposing player; quick picks come in two rows by who won; the partner's sheet has no footer button; the viewer's team comes first with "(나)" everywhere (reverses library §10's slot order in rows); no word between teams in rows ("vs" and "&" only in running text, "대" only in a score read aloud); radio rings lead wherever a footer CTA follows.
    • Faces, members only (migration 00691): one predicate (can_view_face: self, a shared active club, an accepted friend, or an explicit public opt-in; never anonymous), not client-callable; the raw column is revoked; photo storage listing is owner-only. Friends count as consent; an explicit "friends" or "private" still narrows; legacy "public" values written by the old privacy screen are reset. Open: pickup co-participants see initials (strict reading).
    • Sign-up (recommendation, the owner's call): Kakao and Naver everywhere, Sign in with Apple on iOS (App Review 4.8), Google on Android and web, equal size; email for sign-in only; 본인인증 just in time before the first trust action; accounts linked by the verified identity.
  • Owner review 4 — rulings received (2026-10-12). Owner rulings, recorded as given:

    • Approved: no custom implementations (one shared piece per rule), the caption row, the form strip, PersonRow line 2 by context, the AnswerPair 12 / 36 / 12, and the sign-up method (Kakao and Naver everywhere, Apple on iOS, Google on Android and the web, email for sign-in only).
    • Pickup co-players' faces: initials stay. A pickup session is not a membership, so can_view_face is unchanged (self, a shared active club, an accepted friend, or an explicit public opt-in).
    • Retirements and walkovers count; halted, cancelled and otherwise terminated matches need proper outcomes (research lane D of review 4).
    • Scores are sequential: the next match cannot start until the previous one's score is entered and settled, so a dispute is settled at the court, not later (lane D).
    • Sign-up must be non-anonymous because money will move through Toss Payments (escrow-like); follow Korean regulation and practice (lane F). Prefill from providers and support several logins per person (lane F).
    • Open, researched in review 4: the block chevron (needed, or noise?), score entry with less clutter, the match row against ATP's (with avatars), Korean caption wording (lanes D, E and F).
  • Owner review 4 — applied (2026-10-12; rulings: docs/design/owner-review-4-rulings-2026-10-12.md; evidence: research lanes D, E, F of review 4). Logged for veto:

    • Chevron law: a chevron is the suffix of a text that names its destination; never alone, never at a container edge. Blocks: 홈 day "내 일정 ›" (empty "일정 만들기 ›"), ELO "티어 안내 ›" (the tier explainer; the library's "rating detail" did not exist), match state only after its action ("점수 입력 ›"); the identity block is no longer a door, and 공개 프로필 보기 moves to 설정 › 계정. Cells and section-head actions keep theirs; rows that open their own item carry none. Verifier check chevron.
    • Score entry keypad first, no presets (departs from lane D's winner-first chips): two cells per set, auto-advance, the tiebreak cell on 7-6, one stop link, one CTA; 6-4 in three taps.
    • Per-player sequential gate (lane D §3): derived, server-enforced; settled = 확정, 호스트 확정, contested host result, 기록 제외 or an accepted stop; entered-but-unanswered holds the next match; host ways out each record an outcome, no override; gateNudgeMinutes 3 and gateEscalationMinutes 6 replace the 12 h / 24 h timers while a successor is held. New S0h 대기, S7 중단, T30–T33, N19–N21, R14–R19.
    • Stop outcomes (lane D §4): retired and defaulted added beside the existing outcomes, with an explicit winner_side and stopped_reason; retirement rated at half weight once either side has 4 games (retiredMinGames), walkover counts everywhere but is never rated, default as retirement if play started; suspended resumes within 7 days or becomes 미완료; a cancelled match's Tag is 취소됨 (the status rule bans a bare 취소). One policy object mirrored to SQL behind a parity test (no app code this round).
    • Match row: a comparison board (ATP, shipped MatchScoreboard, text, faces) for the owner's pick; default text with an optional faces on team lines; recommendation faces in the 대진표 and sheets, text in results lists.
    • Sign-up: 본인인증 once at sign-up (owner directive 2026-06-02 + review 4 "non-anonymous"; reverses my review-3 "just in time"): provider → 본인인증 → 약관 → 첫 시작; the verified name and birth date replace the self-reported steps; providers give id, nickname, email only; one account per CI; linked logins in 설정 › 계정, the last one cannot be removed. Money through the PG only (TwoMore never holds funds); payees pass PG registration and an account-holder check. Needs counsel: the verification/consent order, escrow duties, minors paying, CI storage.
    • Korean wording: lane F's four revisions (#7, #13, #15, #19); the no-period rule for one-sentence captions kept; legal text verbatim; a short wording guide linked from the terminology guide.
  • Owner review 5 — rulings received (2026-10-12). Recorded as given:

    • Avatars on every match surface: the match block, every match row and the number-pad score entry; one unified match grammar for singles and doubles across all components (research lane H of review 5).
    • 본인인증 once, at sign-up: approved. Next: which verification route (PASS, 토스, 네이버 인증서 or an aggregator) and the cost (lane J).
    • Honor-system scoring, owner's proposal: any participant enters the score; races never overwrite a written score; a participant may appeal; the host or admin can change it. Replaces the confirm, counter and contest machinery with one review flow (lane G).
    • ATP pattern liked; whether doubles take two lines per team or one, with each player tappable and the row tappable, is researched (lane H).
    • Block door: the text and chevron always hold the top right of the top row; the left text wraps or truncates (lane H).
    • MediaBand: size it by the relative-sizing rule (lane H).
    • Juniors and retirement: the owner asks for the best practice (lanes J and G). Pages and flows wait until the components are final.
  • Owner review 5 — applied (2026-10-12; rulings: docs/design/owner-review-5-rulings-2026-10-12.md; evidence: lanes G, H, I, J of review 5). Logged for veto:

    • One match grammar: FvkTeamLines draws every match surface (row, block, sheets, number pad, ResultRow); faces by default (32, doubles two faces overlapping 8 in a 56 slot; initials where can_view_face refuses; dropped from 1.5×); one team per line; team lines 12 apart.
    • Players tappable from the row (departs from lane H's one-target advice): each name is its own stop to the profile, as ATP's names are links; the row's own press opens the match. A pair's record needs a pair-keyed model (backend gap noted).
    • Block door fixed to row 1 at every text size; left text wraps for dates and sentences, crops for places (replaces "wrap under from 150%"). Verifier check door.
    • MediaBand kept at 2:1 as a rule (round(width ÷ 2 ÷ 4) × 4 plus the status bar); 2.4:1 drawn for the owner's comparison.
    • Honor-system scoring (owner proposal, lane G): any participant enters; entered = settled and rated after the 8 s undo; first write wins on score_revision, a second writer's sheet becomes the review request; any player may request a review within 7 days; a non-playing host or official keeps, changes or excludes; undecided reviews stand after 7 days; a review never holds the gate. States 12 → 7, sheets 4 + row → 2 (ScoreEntry enter/appeal/host, host review), pushes 21 → 5, helper lines on ScoreEntry → 0.
    • Retirement: counts as a win for the side that stayed; rated once either side has 4 games, at half weight (config).
    • 본인인증 route: PortOne v2 + KG이니시스 통합인증 (one vendor screen: 네이버, PASS, 토스, 금융인증서, 카카오, 신한, KB모바일, 삼성패스; CI returned except Kakao without paperwork, no DI); Danal phone verification as fallback; Kakao hidden until its CI paperwork is done; no picker of our own.
    • Juniors: under 14 stops; 14 to 18 allowed with card payment off until 19 (transfer on); later a guardian-managed junior profile.
    • Marks: five proposals on the brand board in rank order (투 monogram, toss over the T, seam 2, bounce, four balls); drawn to "두 개 더". The brand meaning conflict (CLAUDE.md and the web share card say "두 명 더") goes to the owner.
  • Owner review 6 — rulings received (2026-10-12). Recorded as given:

    • MediaBand matches the Block, 2.4:1, for visual consistency (owner's pick over the shipped 2:1).
    • Approved: the door always at the top right of row 1; avatars on every match surface; the one match grammar; the honor system's direction.
    • "We don't use an inline text for action": the score sheet's stop link becomes a proper process (a button and its steps); the owner asks whether "경기 종료" is clear and whether a confirmation modal is needed (research lane L of review 6).
    • Tiebreak entry: render the tiebreak with the set that needs it (decided: a small TB cell under that set's column; the separate tiebreak row goes).
    • Roster: the 호스트 and 게스트 badge beside the NTRP chip looks unclean; a better form is researched (lane L).
    • Avatars against initials: the owner asks exactly when each shows; the rule is written on the match grammar board and in library §5.
    • Name and brand: the owner considers renaming for a global amateur-tennis ambition ("I want to take over the amateur tennis market across the world"); names and strategies researched (lane K).
  • Owner review 6 — applied (2026-10-12; rulings: docs/design/owner-review-6-rulings-2026-10-12.md; evidence: lanes K and L of review 6). Logged for veto:

    • MediaBand 2.4:1 (owner's pick): 208 at 390; pin re-centred in the visible area; HERO_BAND_ASPECT noted for the build.
    • A command is a Button (owner: "we don't use an inline text for action"; tighter than lane L's three exceptions): only the Toast's undo stays a text action; Coachmark, Notice, section-head commands, comment and message actions become ghost or sm Buttons; FvkTextAction defaults to link; verifier check text-command.
    • Stopping a match: tonal 경기 중단 beside 점수 저장 (not 경기 종료: every match ends); reason and who on one screen, then the score so far, then an outcome-named commit ("기권 저장"); no extra modal (the outcome step is the confirmation, 8 s undo follows); the rain outcome renamed 일시 중단.
    • Host review: the pair stays; 기록에서 제외 behind the title row's ⋯, with a danger dialog; restorable from the match page.
    • Roster role: 호스트 / 게스트 as a pill on the face's bottom edge; NTRP alone under the name; spoken "{이름}, 호스트, NTRP n".
    • Tiebreak entry: a TB cell under the set that needs it; the separate tiebreak row is gone.
    • When a face shows: the can_view_face rule written on the match grammar board and in library §5.
    • Names: the amateur's club OS as the strategy; Hitmates, TwoMore, Tennio ranked; trademark searches on the top two before any rename (owner's call).
    • Soft-disabled primary keeps a pale ball (brand-weak #EFF9B7, dark #2E3418) instead of gray100: in the new 경기 중단 · 점수 저장 pair, a gray soft-disabled primary was indistinguishable from the tonal button beside it. Applies to every soft-disabled primary (Toss's pale-CTA pattern).
  • Owner review 7 — rulings received (2026-10-12):

    • The appeal sheet's alert spans the container (the shared Notice at the content width, with the sheet's spacing).
    • The tiebreak cell sits to the right of the relevant team's games (the team that lost the tiebreak), replacing review 6's "under the set"; the set column widens on both lines so later sets stay aligned.
    • Better names needed: a deep naming pass (modern naming patterns, 80+ candidates, screening, a new shortlist on the brand board).
  • Owner review 7 — applied (2026-10-12; rulings: docs/design/owner-review-7-rulings-2026-10-12.md):

    • The appeal sheet's alert is the shared Notice at the full content width (its margin had doubled inside the sheet); every Notice in a sheet follows the rule.
    • The tiebreak cell sits right of the losing team's games, 4 apart; the set column widens on both lines (colExtra on the cells context of FvkTeamLines); when the set columns pass 160 (three sets, two tiebreaks), ScoreEntry drops the faces so the names keep their room (logged for veto).
    • Names: the deep pass ranks Rallia, Tenacia, Hitmore (eight on the brand board); one global Latin name with a Hangul reading is recommended; trademark and native checks before any choice.
  • Owner review 8 — rulings received (2026-10-12): the tiebreak cell must make it unmistakable that the user is editing a tiebreak score (research and redesign; the cell stays right of the losing team); names: retrofit common tennis and sports phrases into modern app names (research round on top of the review-7 pass).

  • Owner review 8 — applied (2026-10-12; rulings: docs/design/owner-review-8-rulings-2026-10-12.md): tiebreak entry labelled three ways (a persistent "TB" column label beside the set labels, a live line "N세트 타이브레이크, 진 쪽 점수" while focused, the focus ring and spoken label; merged "N세트 TB" from 1.5×); the placeholder-only "TB" is gone. Names: seven phrase-derived candidates on the brand board (Naishot first); the top 3 across both rounds are Rallia, Naishot, Tenacia; "Rallie Pickleball" exists one letter from Rallia.

  • Owner review 9 — applied (2026-10-13; rulings: docs/design/owner-review-9-rulings-2026-10-13.md): a court-slang naming round (112 slang items, 36 candidates, screened); the brand board's names fold into one "Name candidates" section of the best ten across three rounds (slang, phrases, coined); top 3 TwoMore, Naishot, Rallia; TwoMore is real Korean club slang per the owner, with an Italian native check owed (tumore).

  • Owner review 10 — applied (2026-10-13; rulings: docs/design/owner-review-10-rulings-2026-10-13.md): the organiser's promise goes in the store descriptor and first-run copy, not the name (generic club words are taken in the store; 총무/정모 are home-only nouns; the winning club apps use short sport-free names); the boundary is racket and court sports with tennis as the wedge (expansion by reuse: 정구 → pickleball and padel → badminton → table tennis, squash); the name carries no sport word, one masterbrand, per-market sport descriptor (TwoMore: 테니스 클럽 운영); top 3 TwoMore (meaning stays 두 개 더; the 두 명 더 hearing fits the organiser, owner to rule on its use in copy), Rallia, Hostmore; Naishot benched (praise is a weak mark). No clubs.sport column now: the same one migration later, an unread column today; match_formats.discipline plus the i18n catalog are the insurance.

  • Owner review 11 — applied (2026-10-13; rulings: docs/design/owner-review-11-rulings-2026-10-13.md): the owner likes short names with no surface meaning (Band, Strava). Trends measured (84 global apps, 63 Korean): length is stable (6 letters, 2 syllables); real words out of context and clipped names rose, compounds and visible meanings fell; Korean short winners are two Hangul syllables; Korean tennis apps are long and descriptive. Screen adopted: 4–6 letters, 1–2 syllables, 2 Hangul syllables (3 at most), no opening cluster, foreign or coined over a dictionary word, no surface meaning. Short names ranked Enda (Norwegian "enda en gang", once more), Mazi (Greek "together"), Mera (Swedish "more"); Enda put first over the lane's Mazi because its hidden meaning is the brand's 두 개 더. The board's name section leads with them; TwoMore, Rallia, Hostmore stay for comparison.

  • Owner review 12 — applied (2026-10-13; rulings: docs/design/owner-review-12-rulings-2026-10-13.md): plain two-syllable Korean words that non-Koreans can say (owner's example Maru, their healthcare app; excluded). 145 words, 59 past the pronounceability filter (CV shapes, vowels a/i/u/o/e only); ranked Madang (the yard where people gather), Duri (둘이, the two of us), Dodam (도담도담, growing well); Doran and Boda also on the board. Evidence does not decide Korean versus foreign; the board shows both directions ranked. The evidence lane's "no r/l mid-word" rule is not adopted (ㄹ between vowels is a tap Spanish and Japanese speakers make natively). The owner's order: name first, domains and checks after the pick.

  • Owner review 13 — applied (2026-10-13; rulings: docs/design/owner-review-13-rulings-2026-10-13.md): a knock-out clearance of the four finalists in the app stores, domains, handles and the KR/US/EU trademark registers (classes 9, 35, 41, 42). Madang is usable with conditions (no identical live mark; no app of that name; getmadang.com and @madangapp free; a 변리사 opinion owed on 마당's distinctiveness in classes 35/41). Oreum is blocked in Korea by a 2025 OREUM filing (classes 9, 35, 41); Dodam by a pending 2025 DoDam filing (9, 42); Duri is risky (live DOORI and 두리 marks). Respellings do not escape (Korea compares by sound; Orum is a US company). The board's names show the four verdicts.

  • Owner review 14 — applied (2026-10-13; rulings: docs/design/owner-review-14-rulings-2026-10-13.md): new plain two-syllable Korean words, cleared before shown. 293 generated (arbitrary everyday nouns favoured, the 당근 route, for trademark strength); 29 survived the filters; each cleared on KR (KIPRIS) and US (USPTO) trademarks in classes 9/35/41/42, domains and handles, and meaning. Ten pass and are on the board: Homil, Jongi, Namul, Nancho, Jigap (clear), Dabal, Ibul (mild echo), Madang, Sabal, Sagak (conditions). Ranked Homil (rye; best addresses), Jongi (paper; .com for sale), Namul. EU register unchecked (TMview refused this machine); KIPRIS read by rank only; a 변리사 clearance owed for the pick.

  • Owner review 15 — applied (2026-10-13; rulings: docs/design/owner-review-15-rulings-2026-10-13.md): "not a derivative of 당근" — no food words and no mascot-like object nouns; words with lift instead. 464 generated, 25 cleared on KR/US/EU trademarks (TMview answered this time), domains and handles, and meaning. Passing: Dadok (다독, a pat of encouragement; clear everywhere; dadok.com for sale), Pallang (팔랑), Dorang (도랑); Machim (마침) with conditions. Near misses blocked only by marks: Ajik (아직, class 35), Milmul (밀물, class 41). The board shows the new names, then the earlier non-food names (Homil, Jongi, Nancho, Dabal, Madang); Namul, Sabal, Ibul, Jigap leave the board.

  • Owner review 16 — applied (2026-10-13; rulings: docs/design/owner-review-16-rulings-2026-10-13.md): positive Korean adjectives (health, fortune, a nudge, gathering, good feelings). 365 generated; only 137 have a readable shape (most positive roots use eo/eu/ae/ui/y-vowels); 26 cleared on KR/US/EU trademarks, domains and handles, and meaning. Passing: Bangsil (방실, a beaming smile; clear everywhere; bangsil.com unregistered), Hanchang (한창, conditions), Dangcha (당차, conditions, half-word). The expected words fell: 무탈 and 호감 on marks, 가만 on handles, 팔팔 (팔팔정; "88"), 단란 (단란주점). Board: the three adjectives, last round's Dadok/Pallang/Dorang, Homil, TwoMore.

  • Owner review 17 — applied (2026-10-13; rulings: docs/design/owner-review-17-rulings-2026-10-13.md): the owner's own words cleared (Mokoji in English, 모꼬지 in Korean, per the owner). 또래 (Ttorae) usable with conditions (no live mark; every address free incl. ttorae.com; 변리사 sound check vs 또래오래). 난장 usable with conditions (난장판 first reading). 품앗이 risky (pending 2025 class-38 messaging mark). 모꼬지 blocked in Korea (2026 filings in 9, 35, 41; three gathering apps). 안다미로 blocked (Andamiro Co., Pump It Up). 두레 blocked. The board leads with Ttorae and Nanjang, then Bangsil, Dadok, Pallang, Dorang, Homil, TwoMore.

  • Owner review 18 — applied (2026-10-13; rulings: docs/design/owner-review-18-rulings-2026-10-13.md): the owner likes Dadok but fears "dad ok". Respellings weighed (Tadok, Dahdok, DaDok; "Dad-" forms do not fix it). Correction to review 15: 다독 is crowded in Korea (registered 다독이, 다독임, 다독365, 다독따독, 다독 인더시티 in classes 9/35/41; an identical 다독 refused in class 35 in 2024), so any spelling with Hangul 다독 is risky. Tadok / 타독 (the owner's pairing) is usable with one condition: no identical KR mark, a coined word (not in 표준국어대사전), tadok.app/.kr/.co.kr/gettadok.com free, @tadokapp free; abroad TADOKE is registered in class 9 (US, EU, CN, JP), for a 변리사. The board leads with Tadok; Dadok leaves it.

  • Owner review 19 — applied (2026-10-13; rulings: docs/design/owner-review-19-rulings-2026-10-13.md): Korean words that sound like Latin roots. 투마 is free in Korea (no live mark, arbitrary); Turma abroad is risky (UK Turma Ltd in 9/35/41/42, EU TURMA 35, ES 41; Spanish "testicle"; turma.app/.io/getturma.com registered 2026). Ultari (울타리 + ultra) usable with conditions (three syllables; 울트라 sound check; ultari.app free). Ballo (발로 + ballo) usable with conditions (Valorant shorthand; 발로소득). Blocked: 한판 (a live "동호인 스포츠 클럽 운영앱" of that name), 아마, 판도, 마노 (risky), 마주, 도로 and others. Board adds Ultari and Ballo; Pallang gains its palla root story.

  • Owner review 20 — applied (2026-10-14; rulings: docs/design/owner-review-20-rulings-2026-10-14.md): three-syllable names. 746 typed, 30 survivors, 18 pass trademarks (KIPRIS, USPTO, TMview), domains and handles, and meaning. Leads: Dabusi (다붓이, close together; clear in KR/US/EU; every address but .com free — lead re-checked KIPRIS and RDAP), Obusi (오붓이; a pending Latin Obusi in KR class 35), Narani (나란히; 옆으로나란히 in class 9), Dalmaji (달맞이). Cut: 신바람, 홀가분 (Samsung), 모닥불, 소나기, 사부작 on marks; 무지개 on meaning; 나날이 on the web. The board leads with Tadok and the three-syllable four; Ballo, Nanjang, Pallang and Dorang move to the research docs.

  • Owner review 21 — the name is chosen (2026-10-14; rulings: docs/design/owner-review-21-rulings-2026-10-14.md): Dabusi / 다붓이 ("Let's do Dabusi!!!"). Deep checks: clear in KR, US and EU in all seven classes (9, 35, 36, 38, 41, 42, 45), every KIPRIS row read, sound-alikes 다부시/다부지/따부시 clear; meaning clear in Korean with mild echoes abroad; dabusi.app and most addresses free (.com is a live Chinese brand, 打不死). Risk that sets the pace: a 다붓 messenger app exists and 주식회사 다붓컴퍼니 holds marks in our classes, so file 다붓이 at KIPO soon. Spelling stays Dabusi; the official gloss replaces review 20's. 두 개 더 lives on as a line ("다붓이 모여, 두 개 더"). The code rename waits for the 변리사; a new mark brief is next.

  • Owner review 22 — applied (2026-10-14; rulings: docs/design/owner-review-22-rulings-2026-10-14.md): all brand direction derives from 다붓이; 두 개 더 retired. Display name 다붓이 (KR) / Dabusi; dabusi.app; share line "다붓이 모여, 함께 치는 테니스". CLAUDE.md, the terminology guide and the brand touchpoints updated; the code keeps TwoMore until the rename. The brand board rebuilt: Dabusi and 다붓이 outlines (Pretendard Bold), lockups, every touchpoint, a new brief (R8, R10, four directions) and five marks with tennis elements, ranked Fitted close, 다 on the court, The D strings, Two snug, Close at the net. Fitted close's first wave read as Pepsi/yin-yang; the two-arc redraw reads as tennis but nears the stock ball (R8); hybrids (a D or 다 carrying the ball) recommended next.

  • Owner review 23 — applied (2026-10-14; rulings: docs/design/owner-review-23-rulings-2026-10-14.md): the mark is Fitted close; the wordmark is DABUSI in all capitals (Pretendard Bold, +6% cap-height tracking; +4% clumped). Rule: the wordmark is DABUSI, the name in text is Dabusi. The board's mark section shows Fitted close only (the other four removed; record in review 22); hairline seams at the mark's mask edge fixed (mask extended past the shapes). Trademark: the word mark carries the weight; the device is filed as a composite with the word.

  • Brand rename in code — applied (2026-10-14; owner: "Great, get going."): the app is renamed in user-facing code before the trademark filing, safe because nothing is live. Display name Dabusi with Expo locales giving 다붓이 on Korean devices (iOS CFBundleDisplayName, Android app_name); i18n appName, webSubtitle, welcomeTitle, the under-14 gate name, dataExportShareTitle, the support subject; web metadata, OG lockup (Fitted close + DABUSI) and tagline; the in-app Logo (packages/ui) rebuilt from the brand SVGs; new icon set (icon, adaptive + monochrome, notification, favicons) from scripts/generate-icon-assets.mjs, which now draws from the board geometry; the public design-system copy, boards and brand docs. Kept on purpose: legal and consent text (투모(TwoMore) stays until counsel), twomore.app/emails/twomore:// (until dabusi.app is registered and deployed), all internal identifiers (packages, bundle ids, env vars, storage keys). No app-version bump: icons and display name do not change the JS runtime. A preview build is needed before OTA resumes (the fingerprint guard). Follow-up: OG routes load no font, so DABUSI and Korean text fall back; load Pretendard Bold.

  • Owner decision — the build waits for flat v2 (2026-10-14): "we don't have to work with the new build yet. We are yet to finish the migration to the flat v2. And when we do, we can then update our build, along with the updated mark, and start from there." The 0.12.0 preview build (bae319b1) is not adopted; apps/mobile/ota-compatibility-baseline.json stays on the 0.11.0 build, so yarn ota:preview is blocked by the fingerprint guard until the post-migration build — expected, do not work around it. The next native build ships the flat v2 migration and the mark together, and becomes the new baseline. OG images: routes move to the Node runtime with full Pretendard (500/600/700) loaded — a Korean subset was rejected because rare syllables in names would render as boxes.

  • Owner review 24 — applied (2026-10-14; rulings: docs/design/owner-review-24-rulings-2026-10-14.md): the owner approved the scoring sheet (ScoreEntry, the stop process, the tiebreak cell, the host review sheet), the match grammar (Team lines, Match row states) and the mark; the library marks them approved. Asked "다붓 instead of 다붓이?": no — an app named exactly "다붓" (a Social Networking messenger) is live on the App Store and Google Play and App Store Connect needs unique names; "Dabut" reads "da-butt"; 다붓 is a stem. The name stays 다붓이 / DABUSI.

  • 바투 checked — blocked (2026-10-14; in review 24 rulings): identical live 바투 marks in classes 9 and 42 (CJ ENM), 41 and 35 (several owners); 투바투 is TXT's fan name; batu.com/.app/.kr taken. The name stays 다붓이. The 다붓 entry is corrected: the store rule blocks only the bare name; the same-category messenger is the real reason.

  • Owner review 25 — applied (2026-10-14; rulings: docs/design/owner-review-25-rulings-2026-10-14.md): 마주, 두루, 어울 and their variants cleared. Blocked: 마주/Majoo, 두루, 두루두루, 두루미, 어울림; risky: 마주침, 마주봄, 마주해, Doru, 어울, 한어울; weak only: 어우름, 어울마당. The common warm "together" words are long registered; 다붓이 stays. Method correction: rdap.org returns 404 even for registered .io/.co names, so those need whois; review 18's dahdok.co claim is marked unverified.

  • Owner review 26 — applied (2026-10-15; rulings: docs/design/owner-review-26-rulings-2026-10-15.md): six 홈 questions. The bell is news; 할 일 is what waits on you: the bell counts unread news only (obligations no longer add to it; the amber tone retires); 할 일 shows for anyone with work waiting (members get 점수 입력, 참가비 입금, 회비 납부; officials add 점수 검토), absent when empty; no Notice on 홈 (the score Notice and the dispute Notice are retired: the gate leaves only each player's last match able to wait, and an appeal is a Cell). To-dos stay a section, not inside the Block (one press target, one door) and not behind the bell alone. The FAB only creates; joining already lives on 홈 (rows with a count) and the member's empty Block now opens 번개 찾기 ›. Rows draw the venue's static map (the library cascade; five map layouts in the kit, one per venue). No court-surface cue on the map (recommended; ring, band and pin drawn as an open question; the corner surface chip was removed as undecodable on 2026-07-27). The preview law: a hub section shows at most three rows; 전체 보기 and the total only when rows are hidden. Follow-up logged: 내 경기 should list the same set as 홈 (own sessions plus the clubs' unanswered ones). Code waits for the 홈 migration (bell count, member kinds, dispute banner).

  • Owner review 27 — applied (2026-10-15; rulings: docs/design/owner-review-27-rulings-2026-10-15.md): a recorded session ends with its last score — sessions are recorded (점수 기록) or casual (기록 안 함, no scores, nothing rated; per session, not per player — open to veto); in a recorded session time never closes the record (it reads 점수 입력 대기), the 30-day void and the 7-day entry window are removed, the only exits are a score or a recorded 경기 중단, and a player who owes a score cannot 참석 another recorded session until it is in (library §10 principle 7, A14; the match row's expired state became overdue). The surface pin is canonical (ring and band removed). 내 경기 → 예정 (the 홈 set across all days; 저장됨 folds in; drawn in the composition pass). Joined made explicit and the flat diagnosis: flat v2 changed the containers, not the composition; seven page laws proposed (one dominant answer, edit to 3–4 modules, form follows the question, statements, state order, the action in its module, three levels of scale) and 홈 recomposed as the worked proposal (the Block = your next game; 내 일정 agenda rows; 참여할 수 있는 일정 map rows with spots left and an inline 참석). Other hubs wait for the owner's approval of the laws.

  • Owner review 27 — 홈 composed in full (2026-10-15; the owner asked to see the tab under the seven laws before accepting them): six states on the 홈 board — an ordinary day (the hero is your next game with the faces of who plays with you), a fee due (the hero's door becomes 입금하기), playing now (your match on court; nothing to join), a score owed (the owed match leads; 참석 held with its reason), nothing joined, official (할 일 between your game and your week). New proposal pieces: faces under a day Block, the agenda date column, the match Block's overdue line, a disabled 참석 while a score is owed. Laws still awaiting the owner's acceptance.

  • Owner review 28 — applied (2026-10-15; rulings: docs/design/owner-review-28-rulings-2026-10-15.md): the Block holds the complete next game (faces on ink in zone C; nothing below it); 내 일정 and 참여할 수 있는 일정 stay apart; the Block's subject is your next game with a state order (cancelled > check-in open > fee due > waitlisted > confirmed); live mode (owner's rule): from check-in a player is held in the live page until their last match is settled, so no score is owed outside the session — the review-27 backstop, the greyed 참석, the member 점수 입력 Cell and 홈's live and owed states are retired. "The official page" is 홈 for a club officer (frames relabelled). The owner asked whether a rearrangement is the direction: no — two directions drawn, A (the list recomposed) and B (a composed layout: the Block, a Strip of counts, a seven-day week timeline, a two-up grid of map tiles); recommended B for hubs, A's grammar for real lists.

  • Owner review 29 — applied (2026-10-15; rulings: docs/design/owner-review-29-rulings-2026-10-15.md): owner confirmed direction B for the hubs (A off the board), live mode with only the session chat reachable, and scoring per session. B refined: 내 일정 as the complete week (game days filled on the date, the time and the venue's short name under each, a fee due in the warning fill; the day-picker form drawn as the alternative, not recommended); 참여할 수 있는 일정 tiles lead with the decision (a 2:1 map, the time, the venue, a spot meter of dots, friends' faces, the club or 저장됨); 할 일 rows say who, what and the verb (faces, a statement, its context, an sm tonal 확인/승인/보기) instead of a Strip of counts. New data proposal: a venue short name (first word cut to three syllables, club override).

  • Owner review 30 — applied (2026-10-15; rulings: docs/design/owner-review-30-rulings-2026-10-15.md): 내 일정 loses its count and the day picker, and shows venue then time under each game day; open sessions are compact tiles (venue, time with an exception Tag, the club or 저장됨) and become 홈's Feed (no count, no 전체 보기, a filter in its head); every session row, tile and cell leads with its venue (the Block's time figure kept; open question to the owner); 할 일 returns to lean Task Cells with a deadline Tag only when near. The page grammar (library §1.14, proposal): Header · Lead · Panes · Modules (≤3, bounded) · Feed (≤1, last, open-ended); the header plus at most one pinned context layer (Tabs, else the day header, else the Feed's head), the lowest drawing the hairline. The board's club "Scrolled" frames now pin the Tabs instead of the 게시글 head; the kit's MediaBand gains edge={false}.

  • Owner review 31 — critical evaluation (2026-10-15; docs/design/owner-review-31-rulings-2026-10-15.md): dark and 200% renders of 홈 B exposed defects, all fixed — a game day's numeral invisible in dark (now the canvas colour), the seven-column week collapsing at 200% (reflows to one Cell per game day from 1.5×), tiles cropping the venue at 200% (one a row from 1.5×), a tile Tag off its line (line slot), "저장됨" shown as a source (the source is the club or 번개; saved is a filter), the empty Block's "다음 7일" contradicting the Block's next-game subject ("다가오는 일정"). Grammar gaps written into §1.14: zone-level states, the large-text reflow, panes owning the zones below, a fixed module order, the one-layer rationale. Open: 홈's Feed duplicates 경기 › 예정 — (a) no Feed, open sessions a bounded Module, or (b) a 클럽 소식 Feed after the Band (recommended; drawn as home-b-news). Also recommended: a dark scrim on map images.

  • Owner review 32 — applied (2026-10-15; rulings: docs/design/owner-review-32-rulings-2026-10-15.md): the owner's combined 홈 Feed (소식: club posts, new sessions, results, photos, plus 번개, 대회, 교류전 by interest) drawn twice — with the open-sessions Module (the same sessions appear twice on one screen) and alone (recommended: 홈 = Lead · 할 일 · 내 일정 · Band · 소식). Rules: one item frame (source line + a body in the item's form), reactions only on social items (PostRow noActions), nothing addressed to the viewer, chronological, time-bound items leave when closed, per-source collapse, filter and mute controls. Dark maps: NAVER Static Map has no dark style (official parameter list), so a black scrim at α0.2 dims the raster with the pin above it; never invert. Review 31's 클럽 소식 option is superseded and removed.

  • Owner review 33 — applied (2026-10-15; rulings: docs/design/owner-review-33-rulings-2026-10-15.md): one Feed item layout for every kind, after 당근's uniform listings (SEED List Item: prefix, title, detail, suffix; top-aligned; whole item clickable) — a 96 square thumbnail, the title (two lines), the meta (source, when), the key fact (a Tag only for an exception), the counts at the bottom right (only above zero); one press target, no buttons. Slots by kind: session and 번개 (map, venue, time, people), post (photo or club tile, first lines, comments and hearts), photos, result (winners' faces, score), tournament (trophy, date, D-day). Dark checked; at 200% the thumbnail drops to 56 and the meta may wrap (the reflow rule). Library §5 Feed item (proposal); the PostRow noActions option is removed.

  • Owner review 34 — applied (2026-10-15; rulings: docs/design/owner-review-34-rulings-2026-10-15.md): the 홈 Block names what it points at and gives each fact a row ("다음 일정 | 내일 …" and the door; the time; the venue with a pin; faces and 6/8 참석 with the weather "☀️ 18°" at the far end; the fee row when due). The grey Band is removed (the Feed's head marks the change; §1.14 now says no Band on hubs). Each Feed kind is designed: a date badge on dated kinds ("토 19", "10.18"), a count badge on photo sets ("8장"), two-fact key lines (time | 복식, time | NTRP band, time | venue), a new 교류전 kind (two club tiles), 공지 ending the meta line (PostRow's placement), tournaments with their divisions and registration deadline. 소식 is 홈's Feed in every frame (veto-able); the open-sessions Module stays as the one alternative.

  • Owner review 35 — applied (2026-10-15; rulings: docs/design/owner-review-35-rulings-2026-10-15.md): the item grammar — every Feed item in one order: overline (kind | source), title, facts (when first, then one qualifier), due (only with a deadline, warnText), footer (posted time left, counts right); nothing on the thumbnail (badges, counts and Tag pills gone); dates: events absolute (오늘/내일/weekday/date), posted times relative under 7 days, deadlines D-n within 7 days. Grounded in the list item's overline/headline/supporting slots (Android ListItem, TDS ListRow), timestamp guides (Close, UX Movement, Zurich DS, PatternFly) and NN/g on text over images. The 홈 section cleaned: the pre-B 홈 frames and component, the open-sessions alternative and duplicates removed; B now has Ideal, a fee due, nothing ahead, first run, loading, error, offline cold and cached, club officer, the picker and coachmark, 200% and dark, under the ids the flows board uses (no missing tap). English owed.

  • Owner review 36 — applied (2026-10-15; rulings: docs/design/owner-review-36-rulings-2026-10-15.md): every Feed item is three rows and the footer, 128 tall for every kind — overline (kind | source); title, one line (a post takes two); facts, at most two (when first, then the deciding fact: a deadline that falls before the event in warnText, else the kind's qualifier); footer (posted time, counts). The separate deadline line is folded in; a deadline equal to the event's start (a 번개's) is dropped; short dates ("토 19:00", "10.18 (토)"). Large text may wrap.

  • Owner review 37 — applied (2026-10-16; rulings: docs/design/owner-review-37-rulings-2026-10-16.md): the 소식 kinds defined. A kind is a noun naming the object — never 새 or 내 (the owner asked "내 일정?": no, 내 일정 means joined sessions and is the module above; a session the viewer joins leaves 소식 for 내 일정). Eight kinds with source, entry, title, facts, counts and exit: 일정, 공지, 게시글, 사진, 경기 결과 (one item per session when its record closes), 교류전 from your clubs; 번개 and 대회 by interest. Not in 소식: anything addressed to the viewer, their own games and results, member joins, dues, chat, clubs recruiting. Library §5 Feed item Kinds; the board's "소식 kinds" spec table.

  • Owner review 38 — applied (2026-10-16; rulings: docs/design/owner-review-38-rulings-2026-10-16.md): the owner approved the page grammar and 홈 ("I like the grammar and the home"); library §1.14 and §5 Feed item are no longer proposals. 내 일정 leaves 홈: the next game is the Block, the rest is 경기 › 예정, and open sessions are in 소식; 홈's Modules are 할 일 only, for an officer with work waiting. The 소식 filter by kind: 전체 · 일정 (일정, 교류전) · 번개 · 대회 · 클럽 소식 (공지, 게시글, 사진, 경기 결과). 일정 shows sessions the viewer could still join, never their own, so 홈 never lists a session twice; drawn as "Filtered: 일정". Muting is a long press (MenuSheet), not "an item's ⋯", because the item has no button. Stale 내 일정 and 새 일정 references are cleared from the board and library.

  • Owner review 39 — applied (2026-10-16; rulings: docs/design/owner-review-39-rulings-2026-10-16.md): your sessions are in 일정 (owner; review 38's joinable-only reversed): a joined session stays in 소식 with its state as the deciding fact ("내일 09:00 | 참석"; 참석/호스트 successText, 예비/입금 전 warnText); the 일정 filter runs soonest first. Kinds streamlined to three after tracing the writers: 일정 (labels = sessions.event_type: 일정, 번개, 대회, 리그, 대항전; gathers your sessions, your clubs', standing-guest clubs' guest-open ones, and 번개 at saved courts or play regions), 결과 (one per session from your 일정), 게시글 (club_posts categories 공지/자유/경기후기/소개 and club_media 사진); filter 전체 · 일정 · 결과 · 게시글. Removed for lack of a data source: external tournaments, two-club 교류전 (대항전 is a one-club label), club_events (no screen). Placement table per kind (홈, 클럽 홈, 경기, 기록); 경기 › 예정 narrows to your own sessions (veto-able). Hub pages composed under the grammar: 기록 (ELO Block with a 30-day sparkline; 전적 Strip + heatmap with 분석 ›; 랭킹 around you with its scope Choice; 최고의 파트너 as three faces; Feed 경기 기록), 프로필 (identity Block; 신뢰, 친구, 업적 as badges; no Feed), 클럽 홈 홈 pane (다음 일정 as Feed items; Feed 소식 of this club; 사진 Shelf removed). §1.14 gains five lines (one-kind Feeds use their row; scope belongs to its Module; neighbouring Modules never share a form; the Lead absorbs its detail; a page holds only its own).

  • Owner review 40 — applied (2026-10-16; rulings: docs/design/owner-review-40-rulings-2026-10-16.md): every Block is three rows (A the label and door, B the figure with at most one graphic at its right, C one line of facts), 150 at 1×. 홈's Block went from four rows (196) to three (faces and 6/8 at the time's right; venue and weather share row 3; a fee due replaces row 3); the ELO Block drops its tier bar (166 → 150; the 30-day line holds B's graphic slot; C says the distance). Not the Feed item's 128: three rows at the display figure need 134 with no gaps, so 128 forces the figure to the hero step and the padding off the rail, and the Block reads as a dark list row. Evidence: Apple HIG medium widget 338 × 158 on 390 × 844 and Lock Screen Live Activities 84–160 ("doesn't need to display everything"); metric cards = one metric + context + a link. Kit: FvkBlock day/elo rebuilt, FvkCSpark/FvkCFacePile added, the frame's bar removed, hub copies deleted.

  • Owner review 41 — applied (2026-10-16; rulings: docs/design/owner-review-41-rulings-2026-10-16.md): the three-row Blocks approved; the ELO Block keeps its 30-day line. 클럽 홈 follows 홈's grammar: one 소식 Feed on the 홈 pane holds everything the club does, sessions included; the 일정 pane, the 다음 일정 Module and the onboarding missions are gone; Tabs 홈 · 채팅 · 순위 · 멤버 · 회비; the official FAB is 만들기 (일정 만들기 or 글쓰기). Non-members see the same page: no Tabs, public sessions and photos only, no names, the join action pinned by policy (open, approval with an intro Sheet, invite, pending, rejected); 클럽 미리보기 retires and the browse flow ends on the club page. Partners and nemeses woven: one person, two records (같은 편, 상대); 기록's Module 파트너와 천적 (top three partners in doubles, top three nemeses in singles or doubles, five-match minimum) opens 선수별 전적 (was 상대 전적: everyone with both records, format Choice, callouts 최고의 파트너, 라이벌, 천적, 최다 대결). Data owed: player_matchups needs a format column and a partner aggregate. Kit: PersonRow context people.

  • Owner review 42 — proposal drawn (2026-10-16; rulings: docs/design/owner-review-42-rulings-2026-10-16.md): the club hero researched (NN/g carousels: ≤5 frames, count and position, no auto-advance; Baymard; WCAG 1.4.11 3:1 for controls on photos; Apple 44 targets and ~12 padding; cover ratios 1.91:1 / 16:9) and audited. Proposed, awaiting the owner's pick: A one left axis (logo straddling at the rail, name and facts 12 under it; recommended) or B no overlap; three facts with the level band; an optional tagline; up to five featured photos (club_media featured) with "1 / 5" and the album as the last frame; control gap 12; ThumbHash placeholders (RemoteImage passes none today). Kit: FvkClubHero layout="stacked" and tagline.

  • Owner review 43 — applied (2026-10-16; rulings: docs/design/owner-review-43-rulings-2026-10-16.md): the club hero is B (no overlap; the logo beside the name and three facts "강남구 | NTRP 3.0–4.0 | 멤버 42명"; an optional tagline) on every board; A's code removed. The photo counter specified: "1 / 5", white 13 tabular on ink α0.64 (the least ink that holds 4.5:1 on a white sky; the old neutral Tag turned light in dark and fell to ~3.4:1), 24 tall, r12, rail, 12 up; shown only with two photos or more, never on the album frame, the default cover or while loading; changes when a swipe settles; not a control (the band is one adjustable element). The band holds up to five featured photos, then the album frame (앨범 전체 보기 ›). Circles on the photo 12 apart (pitch 52). New open issue: the status bar on a bright cover (recommended: a dark status bar when the cover's top is bright, measured at upload). The open-issues list is in the rulings.

  • Owner review 43 — follow-up (2026-10-16; same rulings): the owner asked why the counter differed from the icons on the photo. It had been darkened alone (α0.64, for text's 4.5:1) while the circles kept α0.5 (a glyph's 3:1): two shades for one family. Now one on-photo fill, photoCircle ink α0.6, for the circles, the photo counter and the +N overlays (4.8:1 for white 13 text on a white sky; the glyphs gain contrast).

  • Cleanup after review 43 (2026-10-16; owner: "aggregate the open issues… clean up what we've decided on"): one living list, docs/design/flat-v2-open-issues.md (D1–D6 decisions, reviews, data and code C1–C6, outside-code pointers); EXT-17 to EXT-20 added to the External Blockers Register (KIPO filing, brand addresses, 본인인증 quotes, the cockpit's Access gate); rulings 26–42 carry a status naming what superseded them; library §1.14 rewritten as one current-state chapter (no "Added in review N" stack) and the club hero's anatomy as B; stale wording cleared from the boards (the preview page, a veto note); the record page rewritten to current state with the open list as its asks.

  • Owner review 45 — applied (2026-10-16): "Yeap let's retire the pickers. But we are still keeping the logo generator for the club creation wizard, right? Let's continue." D7: the colour-theme picker retires; the app has one scheme (S2 Ball & Ink) and keeps the appearance choice (light, dark, system); a persisted old theme migrates to the default; the three other themes are deleted. The club logo generator stays exactly as it is: the wizard's 대표 색상 HueSlider, the 문양 generator and FlagMedallion live in @twomore/ui and derive from the club's own hue, never from the app themes.

  • W0.3b and W0.4 — decided while building (2026-10-16): (1) 할 일's unpaid-fee count holds only idle holds (pending, not submitted). A submitted transfer (입금 확인 중) waits on the host, so the member has nothing left to do; the deadline already stopped there. (2) A club-event RSVP is withdrawn by its owner alone, with no membership test, so someone who left can still free their seat; the read policy now also shows the caller's own row, because Postgres lets a DELETE reach only rows the SELECT policy shows (the pgTAP case caught it). Still open, logged in flat-v2-open-issues: leaving a club releases neither future session RSVPs nor 행사 RSVPs, so a departed member keeps a capped seat.

  • W0.5 — decided while building (2026-10-16): (1) auto_advance_session_statuses is not changed. Review 27 says the end-time job keeps ending play and never voids a match, and the code already does that (no void or entry window exists anywhere); the build plan's line saying the job would stop completing was wrong and is corrected. The close is derived (deriveSessionCloseGate): a completed recorded session with unsettled matches reads 점수 입력 대기. (2) The host's 일정 종료: the app has no 경기 중단 or match-cancel action yet, so a hard wait would trap a host behind rounds that were never played. The confirm therefore offers 취소하고 종료 when every owed match is unstarted (named and counted, an explicit way out); a started match still needs a score, and complete_session enforces it (matches_unsettled). (3) 할 일 점수 입력 stays until live mode ships (the library had it both retired and listed); it retires with live mode.

  • W0.6 — decided while building (2026-10-16): (1) The ThumbHash is generated on the device at upload, not by an edge function: expo-image 57, already in the binary, ships Image.generateThumbhashAsync, and its placeholder takes { thumbhash }. The upload is a rare officer action and the hash is cosmetic, so device trust is enough; no native change, no function cost. Seeded photos are hashed by scripts/seed-media.mjs (sharp). (2) The top luminance is derived from the stored hash at read time (isBrightCover, threshold 0.6), not stored as a second number: one source, nothing to drift, and seeded rows get it for free. (3) The band takes the featured set first and fills to five with the latest approved, so the plan tier's featured cap (0/0/1/3/6) limits only what officers pick. (4) The club banner, the cover's fallback, gets a hash too (clubs.banner_thumbhash). The 27 default covers (real court photos, some with bright skies) carry hashes too, generated by scripts/club-default-covers.mjs; picking a default as the banner copies its hash.

  • Owner, 2026-10-17 — leaving a club releases the member's RSVPs; a correction: "leaving a club should release the member's current and future RSVPs." The open issue logged at W0.4 was half wrong: session RSVPs were already released on every departure (leave, removal, account deletion) by private.release_member_future_seats (00322/00327/00407): confirmed and waitlisted RSVPs on the club's not-yet-started sessions are cancelled, excused with no strike, and the leaver's scheduled matches are voided. Only 행사 RSVPs were missed. 00698 adds them to the same function: the leaver's RSVPs on the club's open events that have not happened are deleted (an event without a time counts until the end of its day), so a capped seat frees itself; past and cancelled events keep their history. A session already being played keeps its roster (its matches settle by score or 경기 중단); that is how "current" is read. Both confirms (나가기, 내보내기) now say 예정된 일정과 행사 참석은 취소돼요. A fee already paid stays as with any RSVP cancel (refunds open only when the host cancels the session); that policy is unchanged.

  • Owner, 2026-10-19 — answers to the takeover questions (the lead role passed to a new session on 2026-10-19; record: docs/design/owner-review-46-rulings-2026-10-19.md): pushes and deployments are automatic for now ("all pushes and deployments can be done automatically. You have my permission"): commit, push, db push of validated migrations, edge-function and cockpit deploys; pushes never use --no-verify (the owner said no on 10-18); hosted data writes still need the owner's word, except the photo-only seed approved here. KIPO (EXT-17) waits until the product work is done. A paid fee on leaving: leaving counts as a cancellation only before a deadline, set by best practice (ruling to follow). Hosted photos: a photo-only seed step, approved. The stale auto-loaded instructions are fixed and four deny rules added to the local settings. The rebuilt tools may be committed once verified; the stale board height entries are fixed. Accepted: the decisions logged while building W0.3b–W0.6 and 00698's reading of "current"; the cleanup of the leftover branches, the two sibling worktrees and the Dependabot PRs; /wireframes/web-app-2 parked until after mobile flat v2; a docs refresh and cockpit redeploy. The owner's library pass began (review 46: the hub and list-a boards, 28 marks); W2 waits on the owner's answer.

  • Owner, 2026-10-19 — leaving before the free-cancel deadline counts as a cancellation; a ruling (owner: "set a deadline for when leaving can count as cancellation (what's often the best practice policy here?)"). The deadline is the app's own free-cancel window, 24 hours before the start (APP_CANCEL_WINDOW_HOURS, one app-wide window since 2026-08-16, held to SQL by check:cancel-policy). A seat released by the member's own departure (나가기, account deletion) at least 24 hours before the session counts as a free cancellation: a paid fee is owed back, through the same refund obligation a host-cancelled session opens (00307/00684). Inside 24 hours the fee stays with the host, as with any late cancel. A departure the member did not choose (내보내기 by an official) refunds at any time. Strikes are unchanged: a departure stays excused; whether a late departure should count against 신뢰 is left to the 신뢰 foundation (review 46, lane D). Why 24 hours: it is the cut-off members already see for a free cancel, so one rule covers both; Playtomic closes self-cancellation of a full open match at 24 hours (club exceptions only) and charges no-shows the court fee; 플랩풋볼 refunds 100% until two days before, 80% one day before, 20% on the day until 90 minutes, then nothing; Seoul's public sports facilities refund in full until 10 days before. A graded refund (80%, 20%) does not suit a manual bank transfer between members, so the app keeps one cut-off. Not built yet, on purpose: a member who has left can no longer open the club's session to confirm the refund (sessions are members-only), and the 72-hour sweep would then ding the host for a refund nobody can close. The rule ships with the money pages (participants with fees, 내 회비; review 46 M17/M25), where a refund to a departed member gets a place to be closed. Sources: https://playerhelp.playtomic.com/hc/en-gb/articles/19831672824465 · https://playerhelp.playtomic.com/hc/en-gb/articles/43240624037393 · https://www.plabfootball.com/cs/17/topic/367/ · https://www.plabfootball.com/cs/2/topic/34/ · https://m.go.seoul.co.kr/news/2023/06/29/20230629500071

  • W2 research — the lead's calls, two of them for veto (2026-10-19; record: docs/design/research/w2-tag-and-choice-2026-10-19.md). Before the owner's W2 go, a read-only scan of the 128 files that use Badge or SelectionChip (130 and 106 uses) found 46 mechanical, 22 nearly so and 55 that need a design judgment or a W3 primitive, so the lead proposes W2a (the components, the infrastructure, the lint rule, the codemods, the wrappers and the 68 mechanical files) with W2b moving with the pages on a shrink-only ratchet. For veto, the tone map: a closed state (cancelled, withdrawn, waived, draft; the old muted) reads neutral, and danger stays for what needs action now (disputed, severely overdue); the library's Tag table, which lists danger for cancelled, is corrected to match the match row's neutral 취소됨; RSVP confirmed is success weak; 예비 is neutral, as the boards draw it. For veto, the dark neutral fill: the owner's rule of 2026-09-28 (dark badge fills at least 1.5:1) stands over the S2 table's $gray100 (1.26:1 on the dark canvas): in dark mode the neutral weak Tag gets its own fill at 1.5:1 or more with a label that keeps 6.5:1, and light mode is unchanged. Settled by the spec without a veto: weak by default and solid only over photos and heroes; LineSlot and text scaling built in W2a; DropdownChip, RangeChip, RemovableChip and MarkerPill move in W2a, the Count tag and ActionPill in W3; feature code writes only the tones neutral, warning and danger; Choice gets its own ink focus ring until the shared one changes in W3.

  • Owner, 2026-10-19 — yes to all ten recommendations (owner: "I say yes to all recommendations"; record: docs/design/owner-review-46-rulings-2026-10-19.md, "Owner decisions from lanes D to F", and docs/design/research/w2-tag-and-choice-2026-10-19.md). W2 opens as W2a: the Tag and Choice components, their infrastructure, the lint rule, the codemods, the status wrappers and the 68 files that move mechanically; W2b's 55 files that need a design call move with their pages, on a shrink-only ratchet. 클럽 홈 gets no next-session row (review 41 stands). Attendance is recorded in every club and every pickup, the club setting deciding only enforcement. Strangers see the trust tier only. 신뢰 멤버 keeps its name for now. 알림 runs in time sections (오늘, 이번 주, 이전 알림), and reading a row leaves it in place. Chat stays out of 알림. A friend request is answered inside 알림. An urgent arrival counts on the bell. Low-priority pushes get a per-club level (전체, 공지만, 끔). The owner checks the drawn proposals visually before they are built; the two W2 calls logged for veto (closed states neutral, the dark neutral fill at 1.5:1) stand unless vetoed.

  • Owner, 2026-10-19 — the review 46 drawings checked; one shared foundation (record: docs/design/owner-review-46-rulings-2026-10-19.md, "The owner's check of the drawings"). Accepted: the section head, the tabs, the Strip's tiles face and what's-left line, the activity grid's sentence and steps, the badge shelf, the hub pages, and list-a's 출석 현황, 참가비 and 출석 확인. Next: the activity grid compacted to one month at a time ("We don't have to show all these months all at once"), on the kit's calendar month; a copy button for the 회비 account number, as one shared account row; more emphasis for the 회비 page's summary ("the most important"), drawn as a Block; 대항전's items on the Feed item, the board's 도전 과제 renamed 클럽 대항전. A standing rule ("I want our components across pages to have the same shared foundation. That eliminates the possibility of UI/UX divergence as well as hardcoding"): every page composes the shared components, with no page-local variant and no hardcoded value, on the boards (page boards compose only the kit) as in code (W2 to W4); recorded in docs/canon/components.md.

  • Owner, 2026-10-19 — the workstream rules (owner: "as long as our docker has enough processing power left, we can still push through"; then "I accept the proposed workstream rules"). (1) A heavy job runs when the VM has headroom (1-minute load under 6 of 10 cores, at least 6 GB free), at low priority, even beside another project's jobs — this replaces the one-heavy-job-at-a-time rule of the 2026-10-18 handoff for twomore-v2. (2) Pushes are automatic through the full gate, never --no-verify, from a clean tree. (3) At most three agents, never two on one file. (4) After a board batch, the cockpit is redeployed and the record page updated. (5) W2a lands in short phases, each committed green. Recorded in CLAUDE.md (Shared machine, Workstream). The owner also asked for another pass at the forms board's proposed designs against the principles, decisions and the owner's feedback (open issues, § 2).

  • W2a phase 1 — the lead's calls (2026-10-19; record: docs/history/rebuild-log.md, "W2a phase 1"). For veto, the pulse stops everywhere: the library's motion table says the live dot and the skeleton stop after 5 s per mount, so every PulseDot now pulses three 1600 ms cycles and rests solid — the Live tag, the session and tournament headers, the match status dot, the session strip's live chip and the format chooser's resume dot alike (WCAG 2.2.2); the word beside each dot still says live. The skeleton shimmer follows when W3 touches it. Settled by the spec without a veto: the Choice set's visible legend stays with LabeledField until the v2 field grammar (W3), and the set's label is the group's accessible name; a set lays out its rows from the kit's width estimate in its own measured width, so the app and the boards wrap alike in one render; a removable Choice's × is "{label} 삭제" ("Remove {label}" in English); every Tag variant keeps 6.5:1, label on fill.

  • Owner review 46, round 1 — the lead's calls, for veto (2026-10-19; record: docs/history/rebuild-log.md, "Owner review 46, round 1", and the library's § 4 Trust and § 5 PersonRow). (1) A filtered-empty Button is named for where it goes: 전체 보기 when one value filters the list (알림's filter, a single Choice ▾), 필터 초기화 when several filters are set; the library said 필터 초기화 for every case. (2) The current time head pins as a day header does (§1.14's pinned stack); 모두 읽음 and the filter ride 알림's first head and leave with it when the next head takes its place, as the 소식 filter does on 클럽 홈. (3) A declined 사유 인정 요청 reads 거절됨, not 인정되지 않았어요: a Tag is one or two words, and the terminology guide makes declining 거절 / 거절됨; the official's pair is 인정 · 거절. (4) The money family is 회비와 참가비, not 회비·입금: an unspaced middle dot between two words is a glued pair that no-middle-dot-join rejects in i18n, so the boards draw the label the app can ship. (5) The Task Cell is a Module's one door when the head's action is taken (프로필's 신뢰: 등급 안내 › in the head, 기록 보기 under the tiles), not only a 할 일 entry. (6) Two absences block an RSVP by count, as 00640 does today; the weights (0.5 and 1) feed only the officials' 출석 주의 at a weighted 3. (7) A notification's ⋯ has three rows, the third being 읽음 or 읽지 않음으로 표시 (a ⋯ with fewer is not a menu), and deleting a notification asks no Dialog because the Toast undoes it (the overlay rule's S-1 already says an undoable action gets a Toast instead). (8) The 알림 board's tier row reads 활동 멤버, the tier 프로필 and 내 신뢰 기록 draw for the same viewer.

  • Owner review 46, round 2 — the lead's calls, for veto (2026-10-19; record: docs/history/rebuild-log.md, "round 2"). (1) 출석 확인 is 참석 확인, the owner's own "participation confirmation" (M26), since 참석 is already the RSVP and the attended state; 출석 현황 stays the officials' page and 출석 주의 their flag. (2) 설정's 알림 group is one door and nothing beside it: its three switches move into 알림 설정 (푸시 알림 as the page's master switch, 일정 리마인더 folded into the 일정 family, 메시지 내용 표시 under 메시지). (3) The officials' dues page is 회비 관리, not 클럽 회비·장부 (a glued pair the code's lint rejects; the 관리 door already says 회비 관리). (4) Participants, fees and 참석 확인 are drawn on list-a, where those pages live; the session detail keeps its Roster. (5) A member's 홈 shows their 할 일 (참가비 입금, 점수 입력), as 알림 does; the bell stays 2 and 3. (6) In W2a's clubs lane, three sets whose only padding was the page rail were converted (the inset moved to a wrapper), the home-court control is a lone Choice toggle until the forms pass's radio ring, and the dues quick amounts hug their labels at 360.

  • Owner review 46, round 3 — the lead's calls, for veto (2026-10-20; record: docs/history/rebuild-log.md, "round 3"). (1) Changing an image is one pattern: the row's 사진 변경 opens a three-row MenuSheet (사진 찍기, 앨범에서 고르기, and the removal: 사진 삭제, or 이니셜로 표시 for a club logo), for the profile photo and the club logo alike. (2) With 푸시 알림 off, everything on 알림 설정 is inert, quiet hours and club levels included, since no push comes. (3) A count is typed when its likely values lie more than a few taps from the default: 최대 인원 (2–32) and 매월 납부일 (1–28) join 정원 and 모집 목표 as Number fields; 한 세트 게임 수 keeps its Stepper. (4) A best-partner candidate with no attendance record scores a neutral 8 of 15, as chemistry's no-match is 15 of 30 (the code scores 0 today). (5) A best-partner pair of two other members keeps its record Sheet: the score is a person against the viewer. (6) The sign-in press says 로그인하기 on the forms and auth boards: it only signs in, and the join follows. (7) The 17 시/도 are one kit list in the app's order; list-b's filter had its own order.

  • Owner review 46, round 4 — the lead's calls, for veto (2026-10-20; record: docs/history/rebuild-log.md, "round 4"). (1) 납부 주기 is a Picker Cell into a value Sheet of six (매월, 분기, 6개월, 매년, 일정마다, 한 번) on every board: six outgrow a Choice set, and the guide's word is 6개월, never 반기. (2) A club logo's generated mark is its 문양: the removal reads 문양 사용; a person's missing photo stays 이니셜. (3) The club banner joins the image pattern with its own source: 사진 찍기, 앨범에서 고르기, 기본 사진 선택, then 배너 삭제. (4) A page that only ticks says what leaving loses: 멤버 초대's guard reads "고른 친구는 저장되지 않아요". (5) Options of three words or more, or that need their meaning said, are radio rows with the meaning on line 2: 가입 방식 (바로 가입, 승인 후 가입, 초대 전용). (6) On a settings page a Switch that turns a group off hides what depends on it, as a wizard step does: 클럽 설정's 게스트 허용 off hides 게스트 참가비 and 방문 횟수 제한. (7) The wizard's quick-start toggle (빠르게 만들기) is the one Switch in a form that ends in Save: a mode that seeds answers, not an answer. (8) 최대 인원's one hint is its range (2명에서 32명까지); the board's court-based hint (코트 2면 기준이에요) goes, since no code computes it. (9) The venue Cell under a hero is the door alone, 장소 and the venue's name, on 일정 상세 and 번개 미리보기: the hero's pin fact already states the address (this reverses the line 2 the lead's own brief asked for). (10) In a two-line Cell the value takes at most half the row and wraps there: the overflow frame showed a long venue name squeezing 장소 to one character per line. (11) A refund owed reads 환불 대기, the code's state word (refundPendingBadge), on 참가비 내역 too, and 환불 확인됨 once the member confirms it; 환불 예정 stays a sentence, never a state.

  • Owner review 46, round 5 — the lead's calls, for veto (2026-10-20; record: the component library, § 5 MediaRow Content and § 1.15; docs/history/rebuild-log.md, "round 5", once drawn). (1) A session named on another page is its place and its day, never a name: the club for a club's session, the venue for a pickup, the day as the one formatter writes it; the day alone where the page or the row already names the place (멤버 기록; an alert's line 2 after its club). sessionTitle() is the type (복식, 복식 번개), and the boards' 토요 정기 복식 and 번개 복식 go, since no rule writes them. A group chat keeps the name its members gave it. (2) 나와의 전적 opens 선수별 전적 filtered to that person, titled as its door is: the person's row first, the two records as tiles, 최근 5경기 with them, then the matches together; back returns to the page that opened it. (3) 성장 분석 holds the numbers 모집 계획 implies (정원, 필요 정원, 주간 연인원, with the attendance assumption in a caption), which forms batch 3 moved off 클럽 설정; 클럽 설정 keeps the inputs and a door here. (4) A Cell from 1.5× with a value keeps its second line: the label, the value, then line 2, so the reason or fact still ends the Cell. (5) The session's 관리 sheet is titled by its day and start time ("9월 19일 (토) 09:00"): the page above it names the place.

  • Owner review 46, round 5 — the lead's later calls, for veto (2026-10-20; record: docs/history/rebuild-log.md, "round 5"). (6) 초대 전용, not 초대만: the third 가입 방식 is the code's word (common.inviteOnly, the join badge, list-a and the hub); round 4's call (5) wrote 초대만 and is corrected. (7) A session's tool pages name it in their subtitle by its place and its day (참석자 관리, 참석 확인, 호스트 관리 and 경기 규칙: "강남 라켓클럽 | 9월 18일 (금)"); only the 관리 sheet, on the session's own page, is titled by its day and start time, and inside a row's facts the day goes without its weekday. (8) A Caption row has one tone besides secondary: warnText for a shortfall the numbers above it imply (성장 분석's 정원보다 N명 부족해요, as the app draws it); the plan's assumption is the Section head's note. (9) An Empty state's copy is the kit's: a page keeps its own body only where its next step differs (대진표 offline: write the scores on paper) and its own title only where the failure is not a read (a search). (10) Share surfaces name a session as its page does: the venue as the title, the club and the day as the lines. (11) One-page creation forms reveal as wizard steps do (행사 만들기, 장소 정보 제안, 소유권 신청); a step whose every section has a default draws whole (클럽 만들기's steps 6 and 7, the defaulted-axis law), though the app reveals two of those sections on a touch today. (12) A session's own surfaces keep the day and the type in a sentence ("9월 19일 복식 일정을 취소할까요?").

  • R3, the remaining pages — the lead's calls, for veto (2026-10-20; the owner: "start R3's remaining pages … while I check rounds 1 to 5"; record: the component library § 1.14). (1) A detail page's 정보 is the object's facts, not a preview, so a long fact group is one Module and the three-row law does not cut it; a page's facts sit in one group, never a run of headed Cell lists. (2) 공개 프로필 takes 프로필's composition with the match summary for other viewers (review 39): the identity Block and the person's club as a Cell under it, then 신뢰, 전적 (the Strip, then 나와의 전적) and 업적 as a Badge shelf; another person's friends are not shown. (3) 경기 has three panes (review 27, narrowed in review 39): 예정 holds your own sessions by day, and its head's filter keeps 저장됨 for the sessions you saved; 번개 찾기 every open 번개; 테니스장 the venues. (4) A list creates with its FAB; the CTA band is a detail page's and a form's. (5) Counts sit on the heads that section a Feed, never on the Feed's own head. (6) The pages with no Header are the 시작 landing, the version gate and the web pages outside the app; a pushed page's title lives in its Header. (7) 리더보드's scope is its Feed head's Choice, 기록's four, never Tabs. (8) 경기 기록, the standalone list, retires: 기록's 경기 기록 Feed is that list. (9) A departure lives in the header (skip, ×) on a wizard step and a receipt, as the WizardShell header law says; 나중에 할게요 on onboarding becomes the header's skip. R3 waits on the owner's check for the pages in rounds 1 to 5 (the record page's items 16 to 35); its first batch leaves them alone.

  • R3, the lead's later calls, for veto (2026-10-20; record: docs/history/rebuild-log.md, "R3, first batch"). (10) A filter that scopes the whole page sits on its first head (기간 기록's 필터, which the app applies to the record, the 활동, the 상대 and the list; 선수별 전적's format); a Feed's own filter sits on the Feed's head. (11) 공개 프로필's Modules run in 프로필's order, 신뢰, 업적, then 전적, so 신뢰's tile and 전적's Strip never neighbour; a private photo is the initials, and the club stays, since a club is not private. (12) A sheet frame over another board's page keeps the phone's height: the phone clips the page behind it (the measure rules' kept entries for 일정 만들기's entry and 관리). (13) 친구 목록's requests are one bounded Module (three rows, then 전체 보기), answered in their rows, not a door Cell. (14) A head holds one Choice: 리더보드's 기간 is a group of its scope Choice's Sheet. (15) Onboarding's skip reads 건너뛰기, the header's label; the old 나중에 할게요 is the owner's to bring back.

  • Owner, 2026-10-20 — the activity grid by result; the visual checks streamlined (record: docs/design/owner-review-46-rulings-2026-10-19.md, "The owner's check of the activity grid and of the visual checks"). The owner offered a count on hover, or three colours for wins, losses and neutral; both are drawn: the readout is the day's count alone ("1승 2패") and a day takes its result's colour. The lead's calls, for veto: (1) The shade is the margin, wins minus losses (1, 2, 3 or more), not the winning side's count, so a 3승 2패 day reads near even; the readout gives the counts. (2) One result scale for the app, the success hue for wins, the danger hue for losses and the neutral fill for even, as the app's own activityMetricTone colours 기간 기록's calendar; the Form strip's cells take its solids (승 teal, 패 red, where they were ink and grey); a word keeps its tones (the ResultRow's 승 Tag success weak, 패 and 무 neutral; the Feed item's facts), since a loss asks nothing of the reader. (3) A day without a match is its number alone, so the discs are the days played. (4) The sentence adds the month's record ("9월 중 6일 활동, 9승 6패"). (5) The readout drops the date, since the number under it is the date, and is spoken with it ("9월 13일, 1승 2패"). (6) The record page lists only what waits on the owner: accepted items leave it, a redrawn item says so, and the flat v2 boards keep no drawing a change has replaced.

  • Owner, 2026-10-20 — items 4 to 15 checked: four rows, no meta lines (record: docs/design/owner-review-46-rulings-2026-10-19.md, "The owner's check of items 4 to 15"). Approved: the activity grid by result, the copy icon, the dues pages, the Strip. Drawn: 입금 완료 알리기 at the very bottom with no line; every Feed item four rows and 128; no meta lines (library 1.16, the board lint's meta-line). The lead's calls, for veto: (1) In a list of one kind the who row replaces the footer, so the posted time, friends going, seats taken, a club's last activity and its member count leave the item (a session's seats stay as its deciding fact when two or fewer are left; a club's size is its join gate); 홈 keeps its footer. (2) A venue's distance and rating move up, the distance first in the facts while 가까운 순 is on, the rating (★4.3) last in the who row, since both decide the choice. (3) What stays is listed in 1.16: a state that stands in for content (an empty Module's line, the Feed end, a shortfall that asks for action), a Dialog's body, a radio row's meaning, data the page or its action is about (a press's total, a live selection count, the price of joining, a section's own figures), a provider's attribution (네이버 제공), legal text, and an action's failure; each one on the page boards carries a pragma with its reason. (4) A measure moves into its title where its note left (최근 20회 출석, 7일 상승세). (5) A field's rule is its error's text, and a phrase to retype is its label ("“탈퇴합니다”를 입력해 주세요"); the password rule shows when it is broken, not before. (6) The asterisk keeps no legend ("* 필수 항목" leaves).

  • Owner, 2026-10-20 — 클럽 홈, 알림, 내 신뢰 기록 and the forms (record: docs/design/owner-review-46-rulings-2026-10-19.md, "The owner's second check of 2026-10-20"). Approved: the Feed items, the forms with a consistency pass. The lead's calls, for veto: (1) No Block inside a club page (the owner asked what the lead thinks): the band and the hero lead it, and a 150 Block under them left a pane little room; a pane's summary is a Section head over tiles, and pages with nothing on top keep their Block. (2) The Tabs sit 8 under a Lead and 0 under a Header; the pane follows the Tabs rule (24 to a title, 16 to a row). (3) A Section head's command is a tonal sm Button everywhere (모두 읽음, 선택, 수정), so a command never reads as text. (4) 처리할 일 is the alerts that ask the viewer to act while their request is open; a row leaves that view when its request is settled, never when it is read; 홈's 할 일 door opens it. (5) Every filtered empty state reads the same: compact, its title, and 전체 보기. (6) A trust record is a two-line row with its venue's map; the rule, the drop-off day and the request live in the record's Sheet, the request as its second step. 멤버 기록's absence rows (an official answers in the row) keep their lines for now. (7) The forms take one label per field (성격, 실력 범위, 소개, 탐색에 공개, 정원; 최대 인원; 사진 변경), one 실력 control (클럽 기준 그대로 | 범위 설정) and one fee pattern (무료 | 유료, 게스트 참가비 too); a typed phrase needs no second Dialog. Left for the owner: 프로필 수정's 비공개 values, the venue labels (장소, 테니스장, 활동 장소) and the app's 경기 방식 (친선 | 경쟁), which 일정 만들기 does not ask.

  • Owner, 2026-10-20 — the page levels (record: docs/design/owner-review-46-rulings-2026-10-19.md, "The owner's third check of 2026-10-20"). The owner asked for one grammar per level of the hierarchy, a correction of their reading, pages organized by level for audit, and a code on every page. The lead's calls, for veto: (1) Five levels, by kind not depth: L0 Entry, L1 Tab root, L2 Object page, L3 Detail page, L4 Form (library § 1.17), with families inside L3 (summary, list, task, live, reading) and L4 (wizard, form, settings) that fix the lead. (2) No Block below L1, no hero below L2, no Tabs below L2: 회비 관리 and 내 회비 sum up with tiles, the 10-19 emphasis now carried by the tiles' figures; a detail page's views are its first head's filter, a set held apart a door row (메시지 요청). (3) The plan leaves 회비 관리: 회비 설정 is a setting, reached from 클럽 설정. (4) The cap line counts records (불참 n/2건) and 멤버 기록's line sums weights (출석 주의 3.5/3), as ATTENDANCE_COOLDOWN_THRESHOLD and ATTENDANCE_FLAG_THRESHOLD do. (5) A person's hero is FvkClubHero kind person, the face with no band (a face reaches a viewer only through can_view_face). (6) Codes are checked by the build and the levels index is generated from the boards, so it never goes stale. (7) A page's first head sits 12 under the Header (알림's rhythm, now § 1.5's rule), checked by the verifier's under-header. Named exceptions for the owner's call: 스코어카드's live Tabs and match Block; 클럽 가입하기's club hero (review 43, option B).

  • Owner, 2026-10-21 — the four calls of the page levels (record: docs/design/owner-review-46-rulings-2026-10-19.md, "The owner's answer of 2026-10-21"). The owner chose the lead's recommendations: 스코어카드 keeps its live Tabs and the match Block, 클럽 가입하기 names the club as its row, 내 회비 folds into 클럽 홈's 회비 pane, 회비 관리 keeps its tiles. The lead's calls, for veto: (1) Codes are stable once published: the registry's codes are fixed by hand, and a page that leaves keeps its number unused under retired (L3-12 내 회비), which the build refuses to reuse. (2) The pane takes the plans in its own grammar: each plan is a frame of 클럽 홈 with the 회비 tab selected, a head over tiles, the copy row and the band, so a member's dues have one implementation.

  • Owner, 2026-10-25 — the notes on 회비 관리 and 참석자 관리, and the cohesion pass (record: docs/design/owner-review-46-rulings-2026-10-19.md, "The owner's notes of 2026-10-25"). The owner asked for fewer places where interactions happen, for one consistent grammar on every page and between pages, and for no visual clutter. The lead's calls, for veto: (1) One home per action (library § 1.18): the band does the page's job and follows its state; rows are picked by tapping them in the one section the job acts on (Select mode retired); a row is one tap, and on a managing page it opens the person's Sheet; rare commands are the header ⋯; 공유 is a header icon; making is the FAB with a noun (만들기 and its MenuSheet where one page makes several kinds); a section head holds one control (알림's 모두 읽음 beside its filter is the approved exception); counts live in heads; a Notice is a state. (2) One word per thing (§ 1.19): a door reads the title of the page it opens; 수락 · 거절 between people, 승인 · 거절 for an admission (승인 in one tap, 거절 asking), 인정 · 거절 for an excuse; 확인 대기 · 미납 · 완납 for 회비 and 참가비 alike; 알림 보내기; 친구 신청; ELO 변화; 테니스장 for the place; band verbs that name the result. (3) The Level tag stays where the library put it (Roster, Podium, identity, a level run's first ranking row): no lime chip on every row. (4) A door to an undrawn page is kept as owed (정기 일정, 조, 앨범 관리, 행사), never removed with its feature; a list with no page of its own opens in place (N개 더 보기). (5) The object pages share one header (공유, then 저장 where the thing can be saved, then the viewer's ⋯ or the manager's 관리 gear) and 장소 상세's two views share one grammar (no action Strip; 길찾기 is the map's tap; one 정보 group; the band makes a session there). (6) The navigation map is generated from docs/workflows/page-links.json; the hub board's prose map is retired. (7) 입금 계좌 gets a code (L4-21); codes stay stable.

  • Owner, 2026-10-27 — the name is open again (record: docs/design/owner-review-47-rulings-2026-10-27.md). The owner: "We definitely need a new name. Dabusi just doesn't work," then asked for Korean words, or words Koreans know, on tennis, the group, together, sport and inclusion, cute and short allowed. The lead's calls, for veto: (1) the round is Korean first, so the vowel rule of reviews 12 to 20 is lifted; (2) nothing is renamed in code, boards or the store until the owner picks and a 변리사 clears the pick (the user-facing Dabusi of 2026-10-14 stays until then); (3) the shortlist lives on its own page, where the owner marks Like, Maybe or No with a note and the lead answers under each name; (4) EXT-17 and EXT-18 now wait on the new name.

  • Owner, 2026-10-27 — the page review of L0 and L1 (record: docs/design/owner-review-46-rulings-2026-10-19.md, "The owner's page review of 2026-10-27 (L0 to L1)"). The owner marked L0 and L1 on the new page review: four pages approved, nine with notes (본인인증, the shared-link previews and cards, 홈's 할 일, 클럽, 경기, 테니스장, 기록, 프로필). They asked for one filter grammar on every discovery page, the app's own filters ("It's the UI that needs fixing"), a language pass, and a 프로필 that reflects the states a member is in. The lead's calls, for veto: (1) One discovery grammar (클럽 탐색, 경기, 테니스장, 기록's 경기 기록): search is a Header icon at the top right; the filter is the first Section head's one control (필터 ▾, 필터 N ▾ when on); active filters show as one row of removable Choices under that head only when on, defaults included (테니스장's 가까운 순, 번개 찾기's default region). (2) The filters are the app's (the implemented app, read file by file): 지역 is one filter of two chained Choices (the app's RegionDistrictFilter); 수준 is the app's band slider over the five real NTRP bands (2.0–3.0 to 4.5+), not chips; each surface keeps the app's facets in the app's order (경기 regains 경기 방식, 코트 유형 and 참가비; 기록 loses the 기간 and 단복식 the board had invented); the kit carries both pieces. (3) 저장한 테니스장 becomes 저장됨, the 경기 tab's word for saved sessions; the language pass on every page follows the redraws as its own batch. (4) The tier explainer moves inside 내 신뢰 기록 as the owner asked, and 프로필's 신뢰 head opens 내 신뢰 기록 instead of 등급 안내. (5) Shared links show what, not who: a preview shows what, when, where, level and seats, never faces, names, initials or the host; the share card is the page's own Block, full-bleed; live counts stay on the page, not the image. (6) 본인인증 in four taps of ours: social login, one screen that is both 약관 동의 and the start of 본인인증, the verification company's window and PASS (not ours, no result screen of our own), then the stops (failure, a phone not in the member's name, under 14, a duplicate account). Nothing is built; open for the owner, with counsel where marked: the order (one screen with the terms, against the earlier 본인인증-then-약관 ruling; counsel), the channel (Danal, or Inicis only if it lets us store the CI), under-14 refused inside the vendor window, the foreigners' country, verification at sign-up or deferred for invite links, the display name, and the vendor-facing name while the brand name is open. Two questions for the owner: (a) What does 초기화 do? The boards draw it returning the filters to the page's defaults (테니스장's 필터 초기화 goes back to 가까운 순); the app clears everything today. (b) Does the explainer one step from 프로필 meet the rule that it is reachable from every place the tier shows (CLAUDE.md's Trust tier line; docs/canon/trust.md lists the own profile's title badge as a mount point)? From 프로필 it is now one step further, through 내 신뢰 기록; neither the canon nor the rule changes until the owner confirms.

  • Owner, 2026-10-28 — the second pass of the page review (L0 and L1) (record: docs/design/owner-review-46-rulings-2026-10-19.md, "The owner's second pass of the page review (2026-10-28, L0 to L1)"). The owner approved seven pages (L0-01, L0-03, L0-05, L0-06, L0-07, L1-04, L1-05) and wrote six notes: a better treatment for an already-registered email (L0-02); 본인인증 "as ruled before", with PASS, 토스, 네이버 and 카카오 (L0-04); 할 일 "the same UI, but polished" (L1-01); and shared components with one polished grammar for every filter, a date picker and a shared slider (L1-02, L1-03, L1-03c). The lead's calls, for veto: (1) 본인인증, then 약관 (review 4's order): the first pass's one-screen merge is undone; 본인인증 says why before it asks, then offers the four providers as equal buttons that go straight to each one (KG이니시스 통합인증 with directAgency), with 다른 방법으로 인증 behind a quiet link; Danal stays the fallback; the 통합인증 CI question and Kakao's CI paperwork stay with the vendor (EXT-19). (2) An already-registered email gets the same screen as a new one (OWASP's generic response): the mail differs (a one-tap 로그인하기 and 비밀번호 재설정), and email sign-in never says which of the two was wrong; the registered-address mail needs our own Send Email hook. (3) One filter-sheet grammar from the forms' shared pieces: 보기, then Picker Cells for 날짜 (the shared date picker) and 지역, then Choice sets, then band sliders with their Steppers (수준 is the wizards' level band, 참가비 the same slider over the app's fee stops); the sheet's own chips, region chips and edge-labelled tier slider leave the kit. (4) 할 일 polished: each row's tile is its subject and every row fills its footer; HomeTask gains the subject and the 납부 기한 when the page is built. (5) 초기화 returns to the defaults. L1-05's approval settles the first pass's question (b): the explainer one step from 프로필, inside 내 신뢰 기록, stands; CLAUDE.md's "reachable from every place the tier shows" still holds, and docs/canon/trust.md's mount points change when 프로필 is built, since the canon describes the code.

  • Owner, 2026-10-27 — the language pass on every page (asked in the page review: "we really need a good language pass on all pages", L1-03c; "Needs a language pass", L1-05; record: docs/design/owner-review-46-rulings-2026-10-19.md, "The language pass (2026-10-28, every page)", and docs/design/copy-plans/2026-10-28-flat-v2-language-pass.md). Done 2026-10-28: 75 of the 84 pages changed. The lead's calls, for veto: (1) Toss's current partner-app rules where our voice guide differed: ~할까요? with no 시-honorific, the active voice for what the user did, 닫기 as a dialog's dismiss. (2) Eleven new rows in the library's § 1.19 (순위 and 순위표, 최고의 파트너, 혼합복식, ELO, 칭찬, 수준, 가입 방식 and 참석 방식, 승인 대기, 신청 마감, 클럽 찾기, 끝내기, 남은 금액). (3) Three pages renamed: 순위표 (L3-17), 하루 기록 (L3-05), 제안 상세 (L3-33). (4) The cap lines say their rule in words. (5) Kept: the consent rows' 합니다 (the shipped legal strings, until counsel reviews them), the legal documents, the brand labels on the sign-in buttons, the tier names, and the doors' page-title words.

  • Owner, 2026-10-28 — owner review 48's open decisions, answered (record: docs/design/owner-review-48-synthesis-2026-10-28.md § 7). The rule, refined: "Items of the same kind render as a horizontal list if the page has multiple lists of different kinds. Items of the same kind render as a vertical list if there is only one list of the same kind" (library § 1.20 and the verifier's one-item-type check follow; a frame at a larger text scale is skipped, since a horizontal list reflows into rows there). Pages approved before change too when the inspection found something. The join vote: "both majority and no objection should work". 번개: "How about just plain 매치? or 경기?" The tier names: research first. The lead's calls, for veto: (1) 매치, not 경기, which already names the games tab and a played match (in code, pickup stays). (2) The join vote is a club setting of three: 운영진 승인, 멤버 투표 (과반수 찬성), 멤버 투표 (48시간 안에 반대 없음); the voters are the club's members, since the owner did not say.

  • Owner, 2026-10-28 — owner review 48, batch 1: the rules and the shared components (record: docs/design/owner-review-48-synthesis-2026-10-28.md; the owner: "Your reading of my rule is right. Start the redraw with the rules and shared components"). Done: library § 1.20 and the changes to § 1.16 to § 1.18; the Door pill; the tonal 더 보기; no Feed end line; no command on a head. The lead's calls, for veto: (1) The Door pill takes the Choice's geometry, so a head's one control is always a pill (▾ chooses, › goes). (2) N개 더 보기 is a full-width tonal Button, not a pill: a lone small pill in a row of its own would break owner review 3's rule that one small component never takes a whole row. (3) The legal footer links stay text (이용약관 · 개인정보 처리방침), the one text door, with a Toast's action. (4) The Feed end draws no line at all, also after a paged Feed (the record had proposed keeping it there): a list's bound goes in its head's count. (5) One command may sit on a head: a group's bulk version of its rows' one verb (회비 관리's 미납: 모두 알리기, as the owner asked on L3-11); none other.

  • Owner, 2026-10-28 — the page review of L2 and L3, and the patterns across it (record: docs/design/owner-review-48-synthesis-2026-10-28.md; research: docs/design/research/owner-review-48-research-2026-10-28.md). The owner marked L0 to L3 (27 to fix, 36 fine), set the rule that a page holds one item type per list with anything else compact on one horizontal line, and asked for their notes as patterns, investigated before any change. The lead's calls, proposed and not yet drawn, for veto: (1) One item type per list: eight pages change (기록, both 장소 상세, 회비 관리, 멤버 기록, 기간 기록, 가입 심사, 토너먼트), and 홈's 할 일, 클럽 홈's 홈 코트 and 멤버 관리's door rows fold into their lists or onto one line. (2) Merged pages: one 장소 상세 (club and directory variants), one 신뢰 기록 under 공개 프로필 (for oneself and for officials), 가입 신청 into 멤버 관리, 게스트 지명 into 멤버 초대. (3) Action cues: a head door and N개 더 보기 become the filter's quiet pill; the Feed end only after a paged Feed. (4) 알림 reads without 모두 읽음 (read on open and on tap). (5) 장소 정보 제안 내역 leaves the club: by the venue canon only an administrator or the venue's confirmed owner reviews a suggestion. (6) 클럽 대항전 deferred at the owner's word. Five decisions for the owner: the reading of the rule; 기록 and 기간 기록 changing although marked fine; the join vote (who votes, what passes); a successor for 번개 (오픈 매치, 소셜 매치, 반짝 모임, or keep); the tier names (keep and fix the display, or rename).

  • Owner, 2026-10-29 — owner review 48, batch 2: one item type per list, page by page (record: docs/design/owner-review-48-synthesis-2026-10-28.md § 6; docs/history/rebuild-log.md "Owner review 48, batch 2"). The owner's answers 1 to 3 applied: the refined list rule, fixes wherever the inspection found them, and the join vote with both majority and no objection. The lead's calls, for veto:

    1. 장소 상세's 다음 일정 head opens 경기 filtered to the venue (L1-03).
    2. 공개 프로필's 신뢰 head door 기록 보기 shows to every viewer who is not a stranger.
    3. 홈's to-do items carry the overline 할 일 and the club, at most three under 전체.
    4. 지출 추가 lives in the ledger's ⋯ only, and the ledger's Strip has no head.
    5. 멤버 관리's 출석 현황 moved to the header ⋯. The applicant Tags 신청 and 지명 left; 게스트 지명 belongs to 멤버 초대.
    6. 클럽 설정's 가입 승인 shows only under 승인 후 가입.
    7. 기록 keeps no scope or filter: 순위표 holds the scope, and 기간 기록 holds the filter.
  • Owner, 2026-10-29 — the trust tiers become a point system (record: docs/design/research/owner-review-48-research-2026-10-28.md § 7; the owner set aside set A, the plant set, the generic sets and the Naver-ladder sets, then: "How about a point system?" and "Yes, points"). 신뢰 점수, 0 to 100: +1 per session attended and per new 칭찬, −1 for 10 minutes late, −2 for a cancel 12 to 24 hours before, −4 for a same-day cancel or a no-show, 0 for an absence with 사유 인정, −10 for an upheld report; it starts at 50 and stays hidden until 5 sessions; 신뢰 기록 lists every point. The lead's calls, for veto: (1) the weights, a first guess mirroring the attendance policy's 1 and 0.5; (2) the start at 50 and the 5-session reveal; (3) a stranger sees the score alone; (4) only a deduction notifies.

  • Owner, 2026-10-29 — 신뢰 점수 drawn ahead of batch 3 (the owner: "Draw it now"; record: docs/history/rebuild-log.md "Owner review 48: 신뢰 점수 drawn"; library § 4 Trust). The lead's calls, for veto: (1) Lateness counts. Review 46 kept it for officials and out of the tier; the approved point table counts it (−1 at 10 minutes, so a late session nets 0), and the person reads it on their own record after the private nudge. (2) The score keeps the records' 12-month window, so a deduction leaves on the day its row already names. (3) 점수 안내 reuses the Explainer Sheet of 추천 점수 안내 rather than a sheet of its own. (4) The self view keeps its header 내 신뢰 기록, so 프로필's door still reads the page's title.

  • Owner, 2026-10-29 — owner review 48, batch 3: the researched pages (the owner: "Start batch 3"; record: docs/history/rebuild-log.md "Owner review 48, batch 3"). The lead's calls, for veto: (1) A 10-minute grace: a player asks in 참석 확인 only 10 minutes after their own start, the same 10 minutes that makes 늦음. (2) Check-in before a fee: 도착했어요 precedes 입금 on the day, as the session states' order already says (review 28). (3) No Strip on 참석 확인: its heads carry the counts (§ 1.18). (4) 참석 취소 moves to the header ⋯ while check-in is open (stated, not yet drawn). (5) The FAQ's answers are drafts (도착, 참석 취소, 참가비 입금 and the two 신뢰 점수 answers) for the owner or counsel to check; the legal text stays counsel's. (6) The legal pages draw a specimen revision (KR-1.1.0) so the 이전 버전 door has something to open; the shipped documents have one version.

  • Owner, 2026-10-29 — owner review 48, batch 4: live play and tournaments (the owner: "Start batch 4"; record: docs/history/rebuild-log.md "Owner review 48, batch 4"; research § 8). The lead's calls, for veto: (1) 토너먼트 retires into 스코어카드 and 대진표: a tournament is stages of its session's own matches, so one pair of pages serves a session and a tournament, and the winner picks give way to scores and outcomes like any match. (2) LIVE first and about what is in play, its live signal the minutes played, never a live score (scores are entered after a match). (3) A tournament's scope is one Choice ▾ in the first head, never a second row of chips (five scopes; a Choice set holds five and never scrolls). (4) A group ranks by 승수, then 승자승, then 게임 득실 (a Korean club rulebook; the code's pools rank by point difference alone today). (5) No 3·4위전: 공동 3위, as Korean club tournaments run. (6) The console's queue is 다음 경기 and a freed court takes the next match by itself (no button). (7) Not drawn: pausing a court, a full-screen pan-and-zoom of a large draw (the bracket pages by round), and King of Court's tab in the new order. (8) The agent's own: the scope Sheet's title 단계, the heads 조 순위 and 본선 대진, 시간 지난 경기 opening that match's row, and the Live row leaving a finished session.

  • Owner, 2026-10-29 — owner review 48, batch 5: the words (the owner: "Start batch 5"; record: docs/history/rebuild-log.md "Owner review 48, batch 5"). 번개 → 매치 on the boards and in the living docs, the lead's call of 2026-10-28 standing. The lead's calls, for veto: (1) English keeps Pickup (the code's pickup): "Match" would collide with 경기's English. (2) 매치 has one meaning: the final-set tiebreak is 타이브레이크 10점, never 매치 타이브레이크. (3) History keeps 번개: dated records, the code-mirroring wireframes and the app's copy until each screen is built.

  • Owner, 2026-10-30 — owner review 49, batch 1: the spacing system (the owner: "apply audits and fixes in a foundational, shared and sound engineering practices way"; record: docs/design/owner-review-49-synthesis-2026-10-30.md). The lead's calls, for veto:

    1. Each pair's gap. It is the value most frames drew, except where that broke the ladder: the hero's title 24 under its band (it was 49 on 일정 상세), the first Cell 8 and the first row 4 under a Header.
    2. The offline strip's place is under the status bar, above the whole screen, because the app mounts its banner there. Before, it sat under the Header or under the Tabs, varying by page.
    3. The insert law's step is 12 when the block is first under the Header, the Tabs or a head, and 24 elsewhere.
    4. The board verifier joins yarn docs:check and runs only when the boards changed. Where Chrome is missing it notes so and passes.
  • Owner, 2026-10-30 — owner review 49, batches 2 and 3 (record: docs/design/owner-review-49-synthesis-2026-10-30.md; docs/history/rebuild-log.md "Owner review 49, batches 2 and 3"). The lead's calls, for veto:

    1. Money has no all-at-once. Transfers are confirmed one row at a time. Only arrivals have a group command, 모두 확인. 미납's 모두 알리기 stays.
    2. The live ring breathes three times and holds, rather than flickering (WCAG 2.2.2).
    3. Hiding a 소식 item lives in its long press, the one home of an item's commands, with a Toast to undo it. There is no × on items.
    4. 할 일 is a horizontal lane above the feed, and the feed stays vertical (the § 1.20 exception for the page's one Feed).
    5. The Kakao SDK's decode-uri-component is pinned to 0.5.1, and expo-router's CommonJS copy is a dated exception until a fix exists (expires 2026-11-30).
  • Owner, 2026-10-30 — owner review 49, the forms (record: docs/history/rebuild-log.md "Owner review 49, the forms"). The lead's calls, for veto:

    1. 행사's 장소 is Naver's place search alone, since any place can host a 행사. The app's own venues are courts. When Naver finds nothing, the empty state should offer 직접 입력; this is not drawn yet.
    2. The generator uses the code's shape words, 둥근 네모 and 네모.
  • Owner, 2026-10-30 — owner review 50 (the owner: "Do the same what we did with our review on the previous round"; record: docs/design/owner-review-50-synthesis-2026-10-30.md; docs/history/rebuild-log.md "Owner review 50"). The lead's calls, for veto:

    1. Live has its own colour (the owner, 2026-10-30: "Give live its own colour token, apart from the error red"). The S2 role $live, raspberry #A7135E (white on it 7.25:1) and pink #EE82C0 in dark (ink on it 7.83:1), 37 ΔE from the danger red under deuteranopia. The lead's call within it: the hue.
    2. Results fold into 일정. A finished match keeps its score there, so a tournament has four tabs (LIVE · 일정 · 대진표 · 순위) and the 결과 tab retired.
    3. 프로필's 신뢰 section moved to 신뢰 기록, though it was fine in round 49, so every person page reads the same: the Block's 신뢰 door, and the meter one tap away.
    4. 대진 짜기 follows the code. The preview stays, so the draw is a proposal until 저장. Rounds are drawn one at a time. 경기 시작 and N라운드 시작 stay two steps. The host reaches it from an open session.
    5. Words stay the language pass's when the app differs. The applicant's pending band reads 승인 대기, where the app says 신청 검토 중.
    6. Edit Sheets inside a form read 완료, because the page's band holds the one 저장.
    7. A member's sent-but-unconfirmed fee is sessionPayment.pending (warning), as the app keeps 입금 확인 중 warning. A venue receipt's 검토 대기 is waiting (info).
  • Owner, 2026-11-01 — the flat v2 build, W4 batches 2 and 3: the forms (the owner: "implement till completion" and "do research, find research-based answers and implement"; record: docs/history/rebuild-log.md "Flat v2 build, batch 14" and "batch 15"). The lead's calls, for veto: (1) The tone pad is dropped: 클럽 만들기's colour is the hue band's twelve steps, as the reviewed board draws; this reverses the 2026-08-28 note "let users choose from a wider spectrum" (stored saturation and lightness keep their defaults). (2) Any active member may create a 행사, as the database already allows and the board's wall says; who sees the 만들기 entry is the hub's decision. (3) Cancelling a 행사 tells its attendees (a new signal) rather than the Dialog promising less. (4) The house style beats the boards on 하기 words: 클럽 복구, 범위 설정, 공유, 로그인 (the boards retitled). (5) 가입 승인 shows its one stored value, 운영진 승인, until member voting exists. (6) The domain's bounds beat the boards': 정원 2–200, the start Stepper to 23:30, the match-rules defaults 노애드 and 5-5. (7) 경기 규칙's notify line is gone (no board slot). (8) The credential rows leave 클럽 만들기; the tier guide stays behind the Info hint and an explainer Sheet that shows them is a follow-up.

  • Owner, 2026-11-01 — the flat v2 build, schema batch 2b: a club's invite code is members-only (the owner: "implement till completion" and "do research, find research-based answers and implement"; record: docs/history/rebuild-log.md "Flat v2 build, batch 16"). The lead's calls, for veto: (1) Any active member reads the code, not only the club's officials: the club page already offers 공유 to every member, and the code is how a member brings a friend into an invite-only club. (2) The code stays off the device's disk cache: it is read when needed and kept in memory, like the signed media links. (3) No bundle carries the code, for members either: one read path, so a member's club page never holds a value a visitor's would not.

  • Owner, 2026-11-01 — the flat v2 build, W4 batch 4: 일정 만들기 (the owner: "implement till completion" and "do research, find research-based answers and implement"; record: docs/history/rebuild-log.md "Flat v2 build, batch 17"). The lead's calls, for veto: (1) Something to copy opens the review page through an entry sheet; nothing to copy opens the seven steps (the board's SPEC); a venue preset skips the sheet; the club picker for several clubs waits for the hubs. (2) A club's 지난 일정 are its own sessions that already started, the newest three distinct ones. (3) 매월 reads 4주마다: the series steps 28 days; real calendar months are a follow-up if you want 매월. (4) The code's bounds stay: 한 경기 시간 5–120 in steps of 5 and may stay 미정 (the record page's ruling), 최대 인원 by format up to 16; the capacity suggestion has no slot and goes. (5) 종목's Sheet also asks 경기 분위기 (it decides whether the session counts for rating); 혼복 means 혼성, and 남성 or 여성 under 혼복 makes it 복식; 구성 offers 무관. (6) A deadline whose moment has passed is refused. (7) A failed submit is one line under the button; a failed venue link fails the submit rather than saving a text-only place; a role the server refuses is told only officials can create (follow-up: the server lets owners and admins create while the club pages offer 일정 만들기 to manage_schedule roles). (8) A pickup has no deposit-account notice (a question for you: where a pickup's payer is told to send money). (9) Words: 장소 stays the session form's word (the terminology guide, pending your 장소/코트/테니스장 ruling); 응답 기한 stays the deadline's word (the board's 신청 마감 reverts); 수준 replaces 실력 wherever the record shows it; the third group is 참석 (참가 is for fees and tournaments); 계좌 등록 and 기간 설정 follow the house style. (10) 경기 규칙's row names its set (한국 표준 규칙) on the session page too. (11) 반복 shows on every review (반복 안 함). (12) The 종목 Cell keeps the record's formatter (the event type, the mood and the rating line on line 2) rather than the board's two facts in the value; the entry sheet's meet row is one plain line. (13) The takeover's empty result reuses the directory's contribution row (the board draws a Cell). (14) The duplicate-day question is asked when the day is picked, in the Sheet and the step alike.

  • Owner, 2026-11-01 — the flat v2 build, W4 batch 5: the lists (the owner: "implement till completion" and "do research, find research-based answers and implement"; record: docs/history/rebuild-log.md "Flat v2 build, batch 18"). The lead's calls, for veto: (1) Offline still hard-blocks every screen (your 2026-06-01 rule in QueryBoundary); the boards' "cached list under the strip" frames are not built, because the 2026-10-09 board contract and your rule disagree; flipping it is one change in QueryBoundary and FeedList, if you want it. (2) One word: 친구 신청 (every board since the 2026-10-25 cohesion pass); the app's five 친구 요청 strings moved. (3) A block keeps two people out of each other's search and friend requests (00713: the search leaves them out, a request or an accept between them is refused); whether a block should also end an existing friendship is your call (nothing is deleted today), and 공개 프로필's friendship band still reads the old blocked status (a follow-up with L2-06). (4) A message request belongs to its recipient: 메시지 요청 lists only requests you received, and a request you sent stays in your own inbox as a conversation (as Instagram and KakaoTalk keep it); before, a sender saw their own request with 수락/거절 the server refused. (5) 알림 is built on the data that exists: grouped rows (외 N명), a series, 친구 신청 with 수락/거절, 사유 인정, 신고, the 30-day window and chat left out wait for the rest of W0.7 (its own schema batch). (6) The bell clears when you leave 알림 (the rows you saw are marked read on leaving); clearing it on open needs a seen timestamp (W0.7). (7) Delete in 알림 is a swipe (a drag with a mouse on the web, and a 삭제 action for screen readers); no ⋯, Select or 모두 읽음. (8) Row landings follow the board where the page exists (회비 → 클럽 홈 at 회비; 공지 → the post); trust rows keep the profile until 신뢰 기록 (L3-01) exists. (9) Thread times keep the shared ladder (the club chat pane shares the row); the board's clock time for today is a recorded difference. (10) 동의 이력 draws all five states, but nothing writes 거절됨, 갱신됨 or an expiry date today (a PIPA consent task for later). (11) 공지사항 stays app copy (empty until there is something real), read state on the device, a page per notice; a notice push waits until a notice is published. (12) 앨범 groups by session (the 일정 상세 door) and by uploader and day for photos with no session; the 게시글 door waits, because post photos are a separate, unapproved pipeline (should a member's post photo be album material at all?); tiles do not open, since the photo viewer has no board yet. (13) 파트너 추천 scores trust from the trust score and attendance from the last 20 sessions (00712), both neutral 8 of 15 when there is not enough record, and ranks the whole roster; a factor with no record never headlines a row's fact, and the attendance fact reads 최근 N회 중 M회 참석 (the board's 20 once there are 20). (14) A 가입 신청 row still opens the applicant's dossier (the board says 멤버 관리 at 가입 신청, which has no section anchor yet). (15) A notice is read when its page opens (so a push that opens it counts too); notice dates in the copy are YYYY-MM-DD, written out long on screen. (16) 앨범's day groups use Seoul time (a club has no time zone of its own). (17) A failed photo signing now shows the error rather than empty tiles, on 앨범 and on the club cards that show photos. (18) The message inbox's paging fails loudly on a broken cursor rather than quietly starting over.

  • Owner, 2026-11-01 — the flat v2 build, W4 batch 6: 경기 and 기록 (the owner: "implement till completion" and "do research, find research-based answers and implement"; record: docs/history/rebuild-log.md "Flat v2 build, batch 19"). The lead's calls, for veto: (1) 순위표 moves to its own batch with its schema (a period on the three leaderboard reads, a friends ranking, your national rank, tied ranks, a record on every row): schema before pages; 기록's door opens today's leaderboard, now titled 순위표. (2) The tab bar is the kit's, drawn over the pages on every tab (홈's live dot kept), so a button like 매치 sits above it; check it on your phone first (the Android touch area is the risk). (3) One filter grammar, built once: a 필터 Choice on the first head with removable Choices under it, one Sheet (보기, where and when, what, how much) that applies with 결과 N개 보기, the region as one filter (서울 강남구 외 1곳); always-on filter bars retire as each list converts. (4) 경기 opens on 예정; old 내 경기 / 저장됨 links land there. (5) 경기's 검색 is one list on 예정 and 매치 찾기 (sessions and venues, over what is loaded: a server session search is a later batch if you want completeness); on 테니스장 it opens the venue search. (6) Filters stay on the phone over the loaded pages, so 매치 찾기's 결과 N개 counts what has loaded; your saved region changes only when you apply one in the Sheet (removing its Choice is for now, 초기화 returns to it). (7) Four root-cause fixes: the fee filter read the total cost, not the participation fee; 예정 dropped a session you joined more than 90 days before it; 저장됨 showed past and cancelled sessions; removing the region Choice overwrote your saved region. (8) 테니스장 is its own pane (the venue finder keeps only picking); the list has no owner badge or bookmark (both live on 장소 상세), no photo (none is stored for lists yet) and keeps 평일 N원~ (the price's unit is not stored, so no 시간당); 저장됨 with 종류 전체 leaves saved shops out, as 전체 always has. (9) 제안 내역 moved to 설정's 보낸 제안 (the board's place), since the 테니스장 list now ends on one door, 목록에 없는 테니스장 추가. (10) 기록 is four zones that load and fail on their own; a new player (under five matches) sees 전적 and 경기 기록; 30일 is the trailing 30 days; 연승 ends at a loss or a draw; 순위 is your primary club's; 최고의 파트너 and 천적 need five decided matches, and 천적 lists only people you lose to more often than you beat; 경기 기록's 전체 보기 opens this month's 기간 기록 (the whole history arrives with that page). (11) 기록's match cards read your team from the match itself (the history now carries it) rather than guessing from the scores. (12) Words: 매치 replaces 번개 for the pickup kind and 매치 만들기; 순위표 and 선수별 전적 are those pages' titles, which 기록's doors read; 필터 초기화 is every filtered list's way back; 등록된 장소 (일정 만들기's venue search) and 등록된 테니스장 (the 테니스장 search) wait on your 장소/코트/테니스장 call. (13) Offline still blanks the whole page (your 2026-06-01 rule; the boards' cached frames are not built).

  • Owner, 2026-11-01 and 2026-11-09 — the flat v2 build, W4 batch 7: 순위표 (the owner: "implement till completion", "do research, find research-based answers and implement", and on 2026-11-09 "Run till completion … until you've fully implemented, audited and deployed the app"; record: docs/history/rebuild-log.md "Flat v2 build, batch 20"). The lead's calls, for veto: (1) One ranking rule for all four scopes: the doubles rating, among players with at least one doubles match, ties sharing a rank (1, 2, 2, 4); this fixes 지역, which counted people who never played at 1000, and 전국, which ranked singles-only players at the doubles default. (2) 기간 (이번 주, 이번 달) keeps the players with a rated doubles match since Monday 00:00 or the 1st, on your phone's calendar, and the order stays by current rating (the board's 2026-10-11 rule). (3) A record on every row, all time and across every club, only where that person's 결과 공개 setting lets you see it (the 00704 rule your 공개 프로필 already follows); the rating is always shown; 클럽 and 친구 show the 7-day change, 지역 and 전국 the person's club in its place. In 클럽 this replaces the club-only record the old page showed, so a person reads the same in all four scopes. (4) 친구 is you and your accepted friends, without anyone either of you blocked; blocked people also leave 지역 and 전국. (5) 지역 is your 시/도 (the server ranks 시/도, not the board's 서초구), among people whose region you may see; the tiles 내 순위, 상위, 전체 인원 come from the same pool, so they agree with the rows; if you have no rank, only 전체 인원 shows. (6) The page opens on 클럽 and 전체 (기록's door passes no scope); the scope and the period live in one Sheet, 순위 범위. (7) The end line 순위는 상위 50명까지 보여요 shows only when more than 50 are ranked; below 50 you get a 내 순위 row of your own. (8) The old national and regional reads retire (00715) now that 00714 serves every scope; the standings tab and the club's ranking pane keep their old podium and row until their own pages. (9) 클럽 is two words in English: a club reads "Club", the tab and its page title "Clubs" (one key had served both, so English said "Clubs" for a single club). (10) The English title stays "Leaderboard" (the house rule bars "Ranking(s)" in English copy; the board says "Rankings"). (11) The Podium's rings use the app's medal colours (the one medal set in code, as the medal tiles do), not the board's slightly different hexes. (12) A tiles Strip may hold one tile (the library's "Strip of one"), which 지역 uses for 전체 인원 when you have no rank.

  • Owner, 2026-11-01 and 2026-11-09 — the flat v2 build, W4 batch 8: 일정 상세 and 매치 미리보기 (record: docs/history/rebuild-log.md "Flat v2 build, batch 21"). The lead's calls, for veto: (1) 도착했어요 stamps the time of the tap, with 되돌리기 for a few minutes; the board's 언제 도착했어요? Sheet is not built, because four of its five answers let a player type their own arrival, which the server refuses by design (lateness is never typed); a host fixes a wrong arrival on 참석 확인. (2) 대진 짜기 keeps today's format chooser until 대진 짜기 gets its own page; 정산하기 and 참가비 관리 keep opening today's payments roster until 참석자 관리 confirms fees. (3) 신고 reports the host as a person (a report about a whole session is a later schema item); the host's own page has no ⋯. (4) The header shows three icons over the map (공유, 저장, then ⋯ or the 관리 gear), as the board's frames draw. (5) 경기에서 빠지기 lives in the ⋯ (or the 관리 sheet for a host) while the session is live, since the board removed the old live bar and the action needs a home. (6) The fee stays editable until someone pays, then locks with its reason. (7) A seat held for payment has no cancel; the hold runs out by itself. (8) House style beats the board's verbs: 환불 받았어요, 매너 태그 남기기, 초대, 정산, 점수 입력, 시간 설정, 예비 등록, and the 관리 door reads 일정 종료 (one word for one action). (9) 참석 취소 uses the app's real cancellation window (the policy's hours), not the board's "2시간" wording. (10) The end-of-session doors follow today's permissions (finishing for anyone who runs the matches; cancelling and deleting for the host). (11) A cancelled session shows only its cancelled notice, never a payment notice. (12) 매치 미리보기 keeps today's replace for members, the host and approved guests, and a pending applicant on a closed match sees 신청 마감 without a cancel, as the board draws.

  • Owner, 2026-11-01 and 2026-11-09 — the flat v2 build, W4 batch 9: the host tools (참석자 관리, 참석 확인, 호스트 관리) (record: docs/history/rebuild-log.md "Flat v2 build, batch 22"). The lead's calls, for veto: (1) 입금 확인 can be undone for ten minutes by whoever confirmed it (a new server step, like the dues undo); a waiver has its own Dialog and no undo. (2) 모두 확인 on 도착 confirms everyone who checked themselves in, in one step, and the Toast states how many. (3) 첫 참석 marks a player with no confirmed spot on an earlier finished session of the same club, on the host's and officials' list only. (4) 참석 확인 exists only where the club tracks attendance, as today; recording attendance everywhere is your call (open issue M6). (5) A cancelled paid seat reads 환불 대기, derived from the row; whether a single cancellation should open a refund on the server depends on your cancellation policy (a late self-cancel may keep the fee), so it is a money question for you, not built. (6) 정산 나누기 sits inside 참석자 관리 for a session with no fixed fee, shown only to people the server lets write it. (7) The 확인 대기 row reads "{일시} 입금 알림" (신청 is the word for applying to join). (8) 참석 확인 opens 30 minutes before the start, when players can first check in, not 2 hours as drawn. (9) The switcher 참석자 | 참석 확인 replaces the page, so Back returns to 일정 상세; members see neither. (10) A menu shows only what you may do (money: the host or a dues manager; removing someone: owner or admin; deciding an application: owner, admin or match director), never a greyed-out row. (11) 호스트 관리's 승인 is one tap and waits for the server before the row leaves; 거절 asks first; 전체 승인 is gone. (12) The old payments screen is retired; every fee door opens 참석자 관리. (13) 되돌리기 on a confirmed arrival takes back only the host's confirmation (a new server step); before, it also erased the player's own check-in time.

  • Owner, 2026-11-01 and 2026-11-09 — the flat v2 build, W4 batch 10: the money pages (회비 관리, 회비 설정, 장부, 참가비 내역) (record: docs/history/rebuild-log.md "Flat v2 build, batch 23"). The lead's calls, for veto: (1) 회비 설정 has its own page (/clubs/{id}/dues/plan), reached from 클럽 설정, 회비 관리's ⋯ and its setup step; 회비 관리 stays on /dues. (2) 면제 sits in 완납 with its own state line and leaves the "N명 중" count. (3) The month list runs from the club's creation to now, computed on the device (no new read), newest first, named by the plan's cadence (9월 회비, 3분기 회비, 2026년 회비). (4) The officer's own row has no controls (the server refuses confirming yourself). (5) 납부 취소 shows only while the server still allows it, and 되돌리기 lasts ten minutes, the server's window. (6) 회비 설정 edits the recurring plan only: 매월, 분기, 6개월, 매년; 일정마다 and 한 번 wait for your word, since each needs rows the board does not draw (a name, a date); 대상 is always 전체 멤버. An edit never restarts the plan. (7) 장부 builds what the board draws: no opening balance, no CSV export, no date or receipt on 지출 추가 (an expense is dated today); the receipt row says 첨부됨 and opens nothing until a photo viewer exists. (8) 참가비 내역 keeps a fee row whose session you can no longer see, with its amount, and never shows 미납 on a cancelled session. (9) Session fees use the dues words: 미납, 확인 대기, 완납, 환불 대기 (the board's later § 1.19), through one fee-state badge that 일정 상세 reads too. (10) The pages present each moment themselves: no toast for a row's own move, one toast for a single 알림 naming the person, a Dialog that counts or names before 모두 알리기, 회비 면제, 납부 취소 and 이번 달 회비 만들기, and a Dialog naming who when a write fails. (11) The club home's 회비 pane keeps its old behaviour until the hubs wave rebuilds it. (12) Dues paid in part now read as paid in part: the app never read the paid amount, so 일부 납부 could not show; fixed, with a test that answers each read with exactly the columns it asked for. (13) One plan per page: 회비 관리 lists the active recurring plan's rows only; a one-time plan's rows are not on it. (14) One word for the cadence choice: 납부 주기 (the later 회비 설정 board) on 클럽 만들기 too, where it read 회비 주기; create-club keeps 분기마다 and 6개월마다 because its words are read inside sentences (분기마다 5일까지), while the plan picker lists 분기 and 6개월 (copy plan docs/design/copy-plans/2026-11-09-money-pages.json). (15) Shared wording: session fees say 확인 대기 "Awaiting confirmation" and 환불 대기 "Refund pending" like the rest of the app; the ledger's 코트 category is "Courts".

  • Owner, 2026-11-01 and 2026-11-09 — the flat v2 build, W4 batch 11: 스코어카드 (L3-38), the shared live page (record: docs/history/rebuild-log.md "Flat v2 build, batch 24"). The lead's calls, for veto: (1) Three batches for the live pages: 스코어카드 and 스코어카드 운영 are one page with two roles (102 frames, two schema changes of different risk), so this batch builds the page and everyone's view with the slots the operator fills; the operator layer (할 일, the band, ⋯, 교체, the review Sheet) and the retirement of the old match board and tournament board come next, then 대진 짜기. (2) What kind of session the page shows is one rule: a group or knockout stage is a tournament, the 킹코트 style is the 코트승급전, a pickup with exactly one match is a single match (no tabs), the league event type takes the rotation's tabs, everything else is a rotation. (3) 순위 shows only where there is a table: a points format, a tournament's groups, a swiss or compass stage; a plain rotation has LIVE and 일정; a single-elimination tournament has LIVE, 일정 and 대진표. (4) The page opens on 순위 once the session is over (일정 where there is no 순위), on the live tab while play is on, on 일정 otherwise; the recap sits atop that tab. (5) A tournament's waiting matches sit under one 대기 head after the courts, in order: the data does not say which court a waiting match will take, so the board's queue under a court is drawn only for a rotation, whose matches have courts. (6) Minutes in play count on the phone from the match's start; a round started at once shows equal minutes per court until the operator starts matches one by one. (7) 점수 검토 요청 has its own store (migration 00718): one of the match's four players asks, until seven days after the session, with an optional score and no reason; one review stays open per match; one left unresolved past the window stands; the host's answer comes with the operator page. (8) No 8-second 되돌리기: the rating is applied when the score is saved, so an undo could not honestly undo it; a wrong score is fixed by 점수 검토 요청 with the right score. Your call: an undo needs the ratings to wait, or a replay. (9) A player's own match offers 점수 입력 by pressing its Block while it is played, and as a door once it is past its time or the rest of its round is done; a score that landed first turns the entry into the appeal with the player's digits kept. (10) Notes: 노트 남기기 / 노트 보기 under your own settled match; a row carries the note mark, which opens nothing until 테니스 여정 has its page. (11) 결과 공유 shares text for now; 이미지 저장 needs a native build (your W5 build). (12) An official's per-match rules Sheet moves to the operator page. (13) House words over the board: 운영진 for who may review (the board said 운영자); straight quotes and "event" in English.

  • Owner, 2026-11-01 and 2026-11-09 — the flat v2 build, W4 batch 12: 스코어카드 운영 (L3-39), the operator layer (record: docs/history/rebuild-log.md "Flat v2 build, batch 25"). The lead's calls, for veto: (1) The operator page is 스코어카드 with a layer, never a second page: the host and the club's owner, admin or match director (the server's staff) get it at /match-board; anyone else is sent to the scorecard. (2) A host who plays a match is a player on it: 점수 입력, never 대신 입력, and never the one who answers its review. (3) 경기 시작 starts round 1 at once (the roster locks and the ready matches start), and N라운드 시작 starts the ready matches of a round; "ready" (a court, and every player's earlier matches settled) is now checked by the server, not only shown. (4) A waiting tournament match goes to a court by 시작, which refuses a court already in play; a court that frees does not yet take the next match on its own (a follow-up). (5) 그래도 끝내기 cancels the matches nobody scored, never a scored one. (6) Rebuilding the draw is one step (the unplayed rounds replaced together), for 대진 다시 짜기 and a latecomer's 대진에 넣기; round 1 only before anything starts. (7) The host answers a 점수 검토 요청 with 그대로 두기, 이 점수로 바꾸기 (점수 고치기 when the player proposed nothing) or 기록에서 제외; a change replaces the score and recomputes the ratings from that match. The old way, where every player had to agree to a correction, serves no page any more. (8) 기록에서 제외 keeps the score and takes the match out of every record and rating; 다시 기록에 넣기 restores it and recomputes again. (9) After the session ends, the host keeps only the 할 일 icon while a review is open, for the seven days a player may ask (the board draws no frame for this). (10) No 되돌리기 after a save (as on 스코어카드). (11) 할 일 lists, in order: matches nobody scored, open reviews, courts past their time, then players who have not paid (before the draw, for someone who may see money); a match is in one row only. (12) The band names the one next step with its round number from the data, never typed. (13) The board's words over the old guide: 그대로 두기, 스코어카드 운영, the 할 일 rows; the inbox button reads 검토 (the house style bars a bare 하기).

  • Owner, 2026-11-01 and 2026-11-09 — the flat v2 build, W4 batch 13: 대진 짜기 (L3-47) (record: docs/history/rebuild-log.md "Flat v2 build, batch 26"). The lead's calls, for veto: (1) One page for every draw, for the host and the club's officials only, on an open, closed or live session: the first draw, the next round (N라운드 대진 짜기), and 대진 다시 짜기 before round 1; 일정 상세 and 스코어카드 운영 open it, and the chooser, the add-round Sheet and the tournament modal are gone. (2) A 대회's later rounds (전적매칭, 전원부활전, a straight knockout) are drawn here too, from the engine, as a preview to save; the group stage's 본선 stays on 스코어카드 운영. (3) 진출 인원 and 진출 방식 are not built: the server's knockout takes the top two of each group; 조별 인원 and 조 편성 stay, 조별리그 follows 조별 인원 (8 teams = 2 groups of 4), and the group sentence is worded from the real groups. (4) 대회 rows show when the session may hold one (the host, the club's entitlement, nothing drawn yet); a 대회 session opens with the recommended method chosen. (5) No row that does nothing: 고급 설정 holds only 짜는 방법 (자동 / 수동 구성); 만드는 시점, 자동 만들기 and 기록 wait for something to use them; 참석 lists everyone (choosing who plays waits for a people picker and a server check). (6) The shortfall Notice shows when fewer courts are used than booked, with how many more players fill them. (7) The round and the 방식 that made it are saved together, in one step that no longer posts to the club board or sends a session-updated push. (8) The 코트 count runs up to the courts the session booked. (9) The next round of 순위매칭전 or 코트승급전 waits for the last round's scores (the Notice says how many are missing), instead of reseeding silently. (10) The app's words over the board's (만드는 중..., 경기 배정이 완료됐어요 / "Matches ready!"). (11) Kept for now: the old tournament create path for 전적매칭, 전원부활전 and a straight knockout, whose later rounds read its header; a server step that creates the header with round 1 would retire it.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 14: 클럽 홈 (L2-01) (record: docs/history/rebuild-log.md "Flat v2 build, batch 27"). The lead's calls, for veto: (1) Five panes (홈, 채팅, 기록, 멤버, 회비) as the board draws; the 일정 pane folds into 홈's feed and an old 일정 link opens 홈. (2) One page for everyone: a visitor reads the club on its own page (join band, 가입 신청, the full / archived / pending / rejected states, public sessions and photos); the separate preview screen is gone and its old links redirect, keeping an invite code. (3) Seasons are computed when read from the club's rated matches since the season began (no season table is written anywhere today); an earlier season's standings rank by today's rating. (4) One feed model for 클럽 홈 and, next, 홈. (5) The 결과 item comes from one club-wide read of closed sessions (match count, top winner). (6) 회비 pane: your own charge from the recurring plan first; after 입금했다고 알리기 it reads 확인 대기 and the button waits (the board draws no such frame); the amount chips add and never pass what is left; several open charges show a 미납 N건 line (the board draws one charge). An official's 회비 관리 door counts what others reported. (7) The bulk mark-paid is retired (the server function is dropped; 회비 관리 confirms one by one). (8) ⋯ holds only 클럽 나가기: 신고 and 클럽 알림 have nothing behind them yet. (9) The tagline shows under the club name. (10) A post's commands open from its ⋯ (the board's long press is not in the kit). (11) Event items show no seat count until something reads event RSVPs.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 15: 신뢰 기록 (L3-01), 프로필 (L1-05), 공개 프로필 (L2-06) (record: docs/history/rebuild-log.md "Flat v2 build, batch 28"). The lead's calls, for veto: (1) 신뢰 기록 is built now at /profile/trust (another person's by ?userId=), so both profiles' 신뢰 door opens a page; its 점수 안내 Sheet is the one place the score is explained, and the four-tier info Sheet is retired. (2) The friends' faces are one AvatarRow: each face opens that friend's profile; the +N disc is not a door. (3) Another person shows their primary club only (no 외 N곳, no clubs Sheet: nothing lists a person's clubs to others); a club the viewer may not read (a private club) is left out of the facts rather than called "no club". (4) No 테니스 여정 Cell until that page is built. (5) 공개 프로필 shares its link; the stat-card image stays on 기록. (6) The rating basis (NTRP 자가 평가 / 임시) shows on everyone's Block. (7) Your own id on a profile link keeps the self view. (8) The level-up celebration stays unmounted (your call; recommended: the 기록 tab). (9) 00724: 구력 joins the public profile; the attended-session count reaches every viewer while the score is not yet shown; the RSVP cap is one policy row the 한도 Notice reads. (10) A block is not hidden from you: the page stays readable and the friendship band goes for a person you blocked; blocking keeps you on the page. (11) No 전체 보기 › or 자세히 › doors on 공개 프로필 until those pages take a person. (12) Another person's results come from their own reads (a stranger saw an empty history before); 나와의 전적 comes from your own matches. (13) Words: the board's 신청 취소 / 친구 끊기 read the app's existing 요청 취소 / 친구 삭제; tiles say 3회 / 12명 (English "3 events" / "12 people"). (14) Not built until read: 사유 인정 요청 on 신뢰 기록, the meter's month delta, the venue map in its rows.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 16: 홈 (L1-01) (record: docs/history/rebuild-log.md "Flat v2 build, batch 29"). The lead's calls, for veto: (1) 소식 is one server read (keyset paging across every kind, so a page is never thinned; one round trip), not five client reads merged. (2) Hide and mute are your own rows: 숨기기 hides one item, 이 클럽 소식 끄기 mutes a club; 되돌리기 undoes either; host and label mutes wait for a frame. (3) Each 할 일 card opens its subject (that scorecard, that fee, the club's 회비); several owed fees open 참가비 내역; an official's cards count what is waiting even if its 알림 was snoozed. (4) The lane is not capped at five (an official can have nine kinds). (5) The Lead, in order: your match in play, your match for 30 minutes after it ends, your next confirmed game (with 입금하기 while its fee is owed), else empty; a cancelled, waitlisted or check-in-open game is skipped (undrawn states). (6) First run = no club and no joined game. (7) The FAB by role: 매치 for a member, 일정 for an official of one club, a picker for several. (8) Retired from 홈: the 일/주/월 panes, the disputed-match banner (disputes reach you through 알림), nearby pickups (경기 › 매치 찾기) and club discovery (the first-run lane). (9) Not built until drawn: the "소식 N개 더" collapse, the filter Sheet's muted list, a door on the feed's end. (10) 입금하기 joins the sanctioned 하기 words as the transfer twin of 결제하기. (11) 회비 납부 shows owed dues, due or overdue. (12) 소식 reaches back 60 days for posts, photos and results; a session you are not in leaves once full or started. (13) The Lead's day Block shows the relative day without faces or weather for now; an official's 입금 확인 and 회비 미납 cards show the club and the count.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 17: 장소 상세 (L2-04, absorbing L2-05) (record: docs/history/rebuild-log.md "Flat v2 build, batch 30"). The lead's calls, for veto: (1) One venue id: the page takes the directory (spine) id; a club's court id is resolved once and replaced; /directory-venues/… links redirect. (2) Three small reads, one per Shelf (진행 중, 예정된 매치, 홈 코트로 쓰는 클럽), five at a time, public sessions only, names by each player's own audience. (3) The trust Tag reads 인증됨 and shows only for a verified owner (no 확인됨 / 미확인, the venue canon). (4) 코트 구성: a club sees its own count, everyone else the directory's; 코트 수 바꾸기 allows 1 to 40 and keeps 메모. (5) Seen from a club when you are in a club that uses the venue; an official gets 코트 수 바꾸기 and 클럽 테니스장에서 삭제 in ⋯, so the club's venue list rows are plain doors now. (6) No 예약 알림 switch until something schedules the alert (it would be a dead control). (7) 길찾기 opens 네이버 지도, 카카오맵, T map or 구글 지도. (8) No doors on the Shelves; the whole list lives on 경기. (9) A closed venue says why, offers 재개장 제보, and dims. (10) 정보 rows when the data exists; 리뷰 키워드, 블로그 후기 and 다른 이름 leave the page. (11) ⋯ holds 장소 정보 제안 and 소유권 신청 (신고하기 is a kind inside the form); ownership never depends on reviewer state. (12) Eyebrow by facility kind; a shop has no band. (13) Share stays name and address until a public venue link exists. (14) Words: 예약 시작 and 이 테니스장에서 매치 / 일정 만들기 (the board's). (15) A map app that fails to open is told in a Dialog (errors are never toasts).

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 18: the W0.7 remainder (알림 data) and the R retirements (record: docs/history/rebuild-log.md "Flat v2 build, batch 31"). The lead's calls, for veto: (1) One visibility rule for 알림's list, counts and 처리할 일: the last 30 days, not deleted, not chat; a settled row keeps its place under 전체. (2) Chat leaves 알림 (the 메시지 tab badge is chat's only count). (3) Repeats are one row with "외 N명": new sessions per club and series per day, 자유 posts per club per day (a 공지 never groups), new members per club per day, replies with the latest person; a deleted group comes back on a new event. Match and rating rows are not regrouped yet. (4) An 운영진방 post alerts officials only (it reached every member; nothing like it exists on the live server yet). (5) Only the server sends alerts (any signed-in app could create one for anyone before). (6) 친구 요청 and 신고 are request rows (수락 · 거절 in the row; 신고 for the club's officials); 사유 인정 요청 waits for W0.8 (it needs a request record). (7) 클럽별 알림 (전체 / 공지만 / 끔) mutes only a club's low-importance pushes; payments, cancellations and requests always come. (8) The 푸시 알림 switch now works (it changed nothing before). (9) 알림 설정 shows seven rows over the 13 stored kinds: a family is on when any of its kinds is, and its switch sets all of them; nothing is rewritten. (10) Pushes expire with their alert, quiet ones are passive, a group stacks on the lock screen, and the app icon counts unread; time-sensitive pushes wait for the native build's entitlement. (11) The bell is the unread rows, no netting. (12) Two row doors stay as they are (가입 신청 → the join review, 신뢰 → the profile) until their pages take an anchor. (13) R: the whole-history screen retires behind a redirect to 기록 (the all-time list is owed to a 기간 기록 frame); old 참가비 links open 참석자 관리; the old tournament link keeps its resolver; the share links (/m, /p, /lb) belong to the web share page; 경기 규칙 (draft) and 공지 detail routes wait for your ruling.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 19: 멤버 관리 (L3-25 + L3-26), 가입 심사 (L3-32), 게시글 (L3-41), 메시지 스레드 (L3-42), 단톡방 만들기 (L4-09), 멤버 초대 (L4-10) (record: docs/history/rebuild-log.md "Flat v2 build, batch 32"). The lead's calls, for veto: (1) 가입 심사 has no vote: the vote frames are cut (no vote store; 운영진 승인 stays one decision). (2) 멤버 관리 is the officials' page: a regular member who reaches it meets a permission wall (they see members on 클럽 홈); the member Sheet's name opens the profile and 멤버 기록 opens that member's 신뢰 기록. (3) The decision is made on the review page itself (승인 is the band; 거절 asks in its Dialog). (4) 게시글's photos show whole, not two squares with "+N" (the page could not open the hidden ones); no 수정 rows (nothing stores an edit). (5) One word for sending (보내기) and 초대 stays bare ("3명 초대"), per the terminology guide. (6) 메시지 is built without 답장, 복사, the camera and resending a failed message (each needs a store or a device hook first); a message request's row opens the person, the thread only after 수락 (the board).

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 20: 비밀번호 변경 (L4-12), 회원 탈퇴 (L4-13), 공개 범위 (L4-17), 동의 관리 (L4-19), 설정 (L4-15), 친구 목록 (L3-18), 업적 (L3-09), 문의하기 (L3-43), 이용약관 (L3-44), 개인정보 처리방침 (L3-45), and the door fixes (record: docs/history/rebuild-log.md "Flat v2 build, batch 33"). The lead's calls, for veto: (1) The two owed routes are placed: the 경기 규칙 draft route folds into L4-06 and a 공지 detail is a page of L3-22. (2) Seven blueprints rebuilt after their ratification go back to unreviewed (L2-01, L4-02, L4-04, L4-06, L4-11, L4-20, L4-21), so the visual pass sees each rebuilt page once. (3) 홈's 입금 확인 card keeps landing on 일정 관리 (the task carries no session). (4) The house style beats the boards on 하기: the bands read 비밀번호 변경, 회원 탈퇴 and 동의. (5) A club's 대표 cannot reach the 탈퇴 band: the page lists the clubs to hand over first, read by a new owner-only read. The wall is the board's rule; the server needs no new guard, because a 대표's 탈퇴 already hands each club to its earliest 운영진 or archives it (00407). (6) 신청 취소 is the one wording for withdrawing a friend request (친구 목록 and 공개 프로필), since the app says 친구 신청. (7) 친구 목록 has no remove icon: 친구 끊기 lives on the person's profile (one home per action). (8) The night marketing consent sentence joins the one map of consent sentences; sign-up still does not ask it. (9) Trust notices and pushes land on 신뢰 기록, not 프로필. (10) Four doors wait for their targets: 공개 프로필 to 클럽 홈, 선수별 전적 and 경기 기록 (those pages take no person yet; batch 21), and 신뢰 기록 to 회비 관리 (the officials' view is not built); 연결된 계정 stays a still row until L4-16.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 21a: 기간 기록 (L3-04), 하루 기록 (L3-05), 선수별 전적 (L3-06) and the person doors of 공개 프로필 (L2-06) (record: docs/history/rebuild-log.md "Flat v2 build, batch 34"). The lead's calls, for veto: (1) One read for a period or a day: the rows come from the person-results read for everyone, the viewer included; 선수별 전적 stays the viewer's own matches. (2) Another person's 기간 기록 opens from 공개 프로필's 전체 보기, only when that person lets the viewer see their results; a person who shows totals only gets the 전적 tile alone, and a closed record meets a wall ("공개하지 않은 기록이에요", the lead's words). (3) 전체 보기 opens the month, as 기록's own door does; another person's page has no period arrows. (4) 나와의 전적 (선수별 전적 with one person) is the viewer's own matches with them, so it needs nobody's permission. (5) The app's words beat the boards': 종목 / 경기 유형 / 출처 and 종목별 (not 성격, 구분, 형식별), 베스트 파트너 (not 최고의 파트너), 자세히 보기; the boards are corrected. (6) The filter is those three facets, with every value the app has (혼복, 게스트, 리그, 토너먼트 too); the old sort, group, result and status controls are gone. (7) The 활동 measure shows on the month only; ELO 변화 marks a gaining and a losing day by the two series colours. (8) An empty week or month keeps its arrows on the viewer's own page. (9) A result row opens 일정 상세 on all three pages. (10) The club name on 공개 프로필 opens 클럽 홈 (the identity Block's one link beside 신뢰); a private club and "no club" have none; the several-clubs Sheet waits for a read. (11) 천적 needs a losing record, and a person holds one callout only.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 21b: 일정 관리 (L3-27), 신고 관리 (L3-28), 테니스장 관리 (L3-29), 출석 현황 (L3-07), 클럽 대항전 (L3-10), 성장 분석 (L3-03) and the officials' view of 신뢰 기록 (L3-02 inside L3-01) (record: docs/history/rebuild-log.md "Flat v2 build, batch 35"). The lead's calls, for veto: (1) A report is resolved as 확정 or 기각 in one call (00733); a report resolved earlier has no outcome and reads 해결됨 alone; a second, different answer is refused. (2) 신고 관리 never shows who reported, and the club officials' read no longer carries it; the Dialog's months and points are the server's policy rows. (3) Only the invited club answers a challenge, on the server (00734); nobody can change a challenge's status or score by a direct write any more (an official of the proposing club could accept its own proposal before). (4) 승 / 패 and the standing show only when a score is stored; nothing writes scores yet, so a real challenge shows its state and dates only. Whether results are entered by hand or settled from matches is the owner's product call. (5) 일정 관리 omits 입금 확인 N건 and N경기 (no bulk read); 게스트 N명 is counted from one bulk read; a member who is not an official meets a wall. (6) The officials' record is /profile/trust?userId=&clubId=, not the board's club route; the wall follows the server's answer. (7) Officials see no 연속 참석 tile (the server hides it on purpose); the board is corrected. (8) 사유 인정 by an official uses the existing write; the member's request and the 인정 / 거절 answer are not built (no table, function or push). Owner's call whether to add them. (9) 회비와 참가비 on the officials' record opens 회비 관리 with the member's unpaid dues count; no 참가비 figure yet. (10) Wording kept from the app over the boards: a period reads "9월 20일~10월 4일", the standing "2-1 앞서는 중"; 성장 분석's 모집 계획 reads the club's own capacity figures; a funnel rate that cannot be computed is a dash. (11) A duplicate venue pick on 테니스장 관리 adds nothing and leaves the finder open (no toast).

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 21c: 장소 정보 제안 내역 (L3-30), 소유권 신청 내역 (L3-31), 제안 상세 (L3-33), 장소 소유권 (L3-34), 장소 정보 제안 (L4-07) and 소유권 신청 (L4-08) (record: docs/history/rebuild-log.md "Flat v2 build, batch 36"). The lead's calls, for veto: (1) Nothing these pages could do is removed for want of a frame. Kept on kit pieces, for the owner to draw: on 제안 상세 the reviewer's explanation, the earlier and later suggestion, and 정보 보완; on 장소 소유권 the tenure, the transfer link and proposals, the staff's 소유권 취소 with its reason, the review's verification fields and the consent record; on 정보 수정 the sixteen fact editors behind 다른 정보 and the court sheet; on 소유권 신청 the venue pick step and the staff's 운영팀 검토 row. (2) The own suggestion history stays readable offline, without its actions; the staff's review list is never read from the cache (it holds reviewer-only evidence). (3) 운영팀 검토 is offered to everyone, and a member who is not staff meets a wall (no staff flag reaches the client). (4) 다시 보내기 and 다시 신청 open a fresh form filled from the declined one; the server forbids replacing a declined receipt. (5) 코트 변경, 운영 상태 and 문제 신고 are reached from 다른 제안 inside 정보 수정. (6) A new venue's place comes from the place search; the pin editor stays only in 정보 수정's 위치 sheet. (7) 검토 중인 제안이 있어요 is decided from the viewer's own pending suggestions, and only once that list has loaded. (8) Words: 취소됨 (not 철회됨), 새 제안으로 바뀜, 새 테니스장; bands without 하기 (제안 취소, 검토, 정보 보완, 제안 보내기); the note holds 1,000 characters, the server's limit; 휴장 / 폐업 / 장소 이전 / 재개장. (9) A decided suggestion's notice and push open 제안 상세. (10) Ending your own ownership asks no reason; a staff revoke requires one. (11) A failed send shows the page's own sentence, never a raw server message. (12) 휴장's start date is not asked (the data has no field for it).

  • Owner, 2026-11-10 — two rulings given in the session. (1) 클럽 대항전 results are settled from matches (not entered by hand); which matches count toward a challenge is the lead's to propose and record before building. (2) 사유 인정 요청: the owner takes the recommended direction on every open call of the workflow examined that day: officials answer for a club session and the host for a 매치 with no club, never the requester themselves; the request can be made at the moment of cancelling inside the penalty window as well as from the record afterwards; the reason is a category (아픔·부상, 가족 일, 업무, 교통·날씨, 기타) with an optional short note, read by the deciders only and deleted when the record ages off; a request is made within 7 days of the session; the deduction stands while a request waits and reverses on 인정; no automatic decision (one reminder to the deciders after 3 days); the decider sees the member's excused count of the last 12 months; absences only (late cancel, no-show), not lateness; an answer can be undone for 10 minutes; fees are untouched; 거절 is final (once per record). The steps the board does not draw (asking at cancel time, the categories, the decider's history line) are drawn first. Found while reading: excuse_attendance_record lets an official excuse their own record; the same schema batch closes it.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 22: 정기 일정 (L3-48), 정기 일정 수정 (L4-22, a new page), 조 (L3-49) and 앨범 관리 (L3-50) (record: docs/history/rebuild-log.md "Flat v2 build, batch 37"). The lead's calls, for veto: (1) A regular meet is edited on its own page (/clubs/[clubId]/regular-meets/new and /[meetId]), reached from 정기 일정's rows and its add button; the old editor Sheet retired. (2) The page is named 정기 일정 수정, not 편집 (the app's one word for editing); the board and the page registry follow. (3) 종료 없음 stays possible: the time Sheet's end is optional, so a meet saved without an end keeps none. (4) Picking a 장소 links the club's own venue only when the club already has that venue; otherwise the name alone is kept and the old link is cleared. The page never adds a venue to the club. This closes the old editor's defect (it changed the name but kept the earlier venue's link). (5) The first date moves to today only when the repeat changes to 격주 or 매월; a weekly meet keeps its anchor. (6) 매월 on a regular meet is a real calendar rule (the chosen weekday of the first to the last week of each month; the data already held it), which settles the owner's 2026-11-10 decision 17 for regular meets; a session's own repeat keeps reading 4주마다. (7) 조 follows the board: the tier dot and the eligibility note are gone. 재편성's preview waits for the ratings before 확정 can be pressed (the old preview computed with empty ratings while they loaded and could offer to unassign everyone). (8) 앨범 관리: an upload batch ends in one summary Toast and a failed batch is a Dialog naming the count; the 대표 사진 row is hidden when the plan allows none and locked at the cap; a photo that is waiting opens the same Sheet without 순서. (9) A member no longer reads a photo that is waiting or was refused (migration 00735: approved photos for members, their own uploads for the uploader, everything for officials; the stored files follow the same rule). The board did not ask for it; it was a privacy hole found in research. (10) Reordering photos is one write that either lands whole or not at all, replacing two writes that could tie. (11) A failed write is never a Toast on these pages: the meet page says it under its band, 조 and 앨범 관리 in a Dialog over the surface that stays open. (12) No add button while a page is still loading (the role is not known yet), on all three list pages; the three loading frames were corrected. (13) Words: 만들기 has one key (common.create); the delete question reads 이 정기 일정을 삭제할까요?.

  • Owner, 2026-11-10 — nineteen decisions ("Agree with all recommendations"). (1) The club on 공개 프로필: the person block's club stays a plain fact (the 2026-11-01 ruling stands; batch 21a's link inside the block is reverted) and a separate club row under it opens 클럽 홈. (2) 대항전 counts a match only when it is played in a session tagged to the challenge, with one club's members on each side; friendly games and players who belong to both clubs do not count. (3) 대항전 is built as a whole loop (propose, tag sessions, the other club's members joining, settle from matches, results), as its own batch after the page batches. (4) Dues plans gain 한 번 (a one-off charge with a name and a due date); 일정마다 is not built. (5) 지출 추가 gets a date and an opening balance first, then a receipt photo and the CSV door. (6) Refunds: a paid player who cancels 24 hours or more before the start opens a refund for the host to settle; a later cancel keeps the fee unless the host chooses otherwise. (7) A 매치 with no club: the host enters an account for that 매치, shown only to people holding a seat. (8) Scores: no 8-second undo; the person who entered a score may correct it for 10 minutes. (9) The level-up celebration shows on the player's own scorecard right after the rated match, and on the 기록 tab if not yet seen. (10) 대진 짜기 gains 진출 인원 and choosing who plays; 진출 방식, 만드는 시점, 자동 만들기 and 기록 are not built. (11) 클럽 홈's ⋯ menu gets 클럽 알림; 신고 stays out until a club can be reported to the platform. (12) Reports about content inside a club (posts, comments, club sessions) also reach that club's officials (신고 관리). (13) A block ends an existing friendship, silently. (14) 가입 심사 keeps one 운영진 decision; no votes. (15) 일정 관리 gets the bulk read for 입금 확인 N건 and N경기. (16) Offline: cached content shows under the offline strip everywhere, with actions disabled (this replaces the 2026-06-01 rule that blanked the screen); private reads (staff queues, ownership claims) stay hidden. (17) A session's 매월 repeat keeps reading 4주마다; real calendar months were to be decided with the regular-meet editor (see batch 22, call 6). (18) 연결된 계정 (L4-16) is built with provider sign-in at W5, not in batch 23. (19) AI 요약 on 테니스 여정 stays hidden until a summarising function exists. The follow-up batches these create (small UI; money; scores and draws; reports and social; 사유 인정 요청; the 대항전 loop) run after the page batches and before the release gates.

  • The lead, 2026-11-10 — a defect of batch 21c corrected (ec6029a44). Three handlers in the venue suggestion and ownership pages caught every failure, two of them under a self-declared boundary: marker, which the gate accepted. They now handle the typed failures they understand and rethrow the rest, with tests; and a boundary: no longer passes by being written: each one is recorded by hand in scripts/engineering-baseline.json with its reason (10 today), and a new one fails the gate.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 23: 테니스 여정 (L3-08) and 클럽 테니스 여정 (L3-51), both new pages (record: docs/history/rebuild-log.md "Flat v2 build, batch 38"). The owner's rulings applied: AI 요약 is not built (no summarising function exists); 연결된 계정 moved to W5. The lead's calls, for veto: (1) A note row opens that match's result. The links table gains that door and loses the opposite one (the result's note glyph → 테니스 여정), because the glyph opens the note Sheet on the board and in the app. (2) 테니스 여정 reads past fifty notes, a page at a time by whole days; a journey that silently dropped its oldest notes would be wrong. The board draws no "more" control, so the list simply continues. (3) 클럽 테니스 여정 is a club-plan page behind a new entitlement (off on the free and player plans; on for basic, pro and club plus). The gate is in the app only, as 성장 분석's is. The page waits for the plan read before it shows the plan Sheet, so a paying club never sees it flash. (4) The plan Sheet has no button for anyone until a club plan page exists; the board's button for officials waits. Closing the Sheet returns to 클럽 홈. (5) Someone who is not a member meets the permission wall, not the plan Sheet. (6) Photos by day come from one small read in the club's own timezone that counts approved photos only and signs no file; the month's results come from the existing results read with a month window (never one read per session). (7) A result's title is derived from its session, never the stored free text; the winner's name is resolved at read time. (8) Mood words and glyphs, and the day header, have one source shared with the scorecard and the record pages.

  • The lead, 2026-11-11 — who filed a club report is closed to the club's officials (migration 00736). An owner or admin could read the reporter of their club's member reports through the data API: the row rule let them read the rows and the grant covered every column. 신고 관리 never showed it, but only because the app did not ask. The grant is now a list of the eleven columns that page reads; the reporter and the link to the platform report are read by server code only, and a database test names exactly the closed columns. Found by the research for the owner's decision 12 and verified on the local database before the change.

  • Owner, 2026-11-11 — two agreements for batch 24 ("I agree with your decisions"). (1) 온보딩's step 1 without 본인인증 is an interim typed step (태어난 해 and 이름), drawn on the board first and replaced when the verification vendor exists. (2) The swipe-through value cards before sign-up retire (they are not on the board).

  • Owner, 2026-11-01, 2026-11-10 and 2026-11-11 — the flat v2 build, W4 batch 24: the entry pages (공유 링크 L0-06, 온보딩 L0-04, 이메일 로그인 L0-02, 업데이트가 필요해요 L0-05) (record: docs/history/rebuild-log.md "Flat v2 build, batch 39"). The owner's rulings applied: provider sign-in, the start page, the mailed-link sign-up and 연결된 계정 wait for W5; 온보딩's step 1 without 본인인증 is an interim typed step (태어난 해 and 이름), drawn on the board first; the swipe-through value cards retire. The lead's calls, for veto: (1) Only the email sign-in page is built of the three sign-in pages; it stands where the old login screen stood, with no back (there is no start page before W5), and the web login page is the same screen. (2) A wrong email or password says so in one line under the password; any other failure keeps the general line. (3) 온보딩 is the board's three steps: the interim step 1, 약관 동의, 첫 시작 (클럽 찾기, 매치, 클럽 만들기, 건너뛰기). The play-style step and the closing step retire (nothing read the play style). 첫 시작 is skipped for someone who signed up from a link and already has somewhere to go. (4) A signed-out person can open the terms and privacy documents on mobile and come back with what they typed kept. The document opens over the sign-in page or the wizard, because a navigation request made before the app's screens are mounted is dropped (read in the router's source); a link that points straight at a document opens it too. (5) A share link shows the preview to everyone who is not a member, the host or an approved guest; a signed-in stranger of a public 매치 used to be sent to 일정 상세. The preview never says who (no host, no names, no faces). (6) 참석 신청하기 on the preview opens the existing apply page, only when the session takes applications and is not closed; a closed session shows 마감된 일정이에요 with 매치 찾기; a session that takes no applications shows neither. (7) A shared result shows the score, the format and the finish time to non-members; an open review reads 검토 중 (never 분쟁); a voided match reads 기록 제외 with no numerals. (8) The preview read gained ten keys (the fee, whether and how a session takes applications, its venue; a match's venue, format, outcome, status, open review and event type), all additive, none naming a person; a database test holds the payload to a list of allowed keys. (9) 업데이트가 필요해요 was already built; only its words changed: the button stays 업데이트 (the app's rule on 하기 wins over the board) and the sentence shown when there is no store link names no app name until the name is filed. (10) The four share links (/s, /m, /p, /lb) share one loading, not-found, offline and error frame. (11) A failed consent write fails the sign-up's finish: required consents are on record before onboarding completes, and 다시 시도 sends the writes again. (12) A page that presents its own failed write treats only a typed refusal as a refusal. The helper the profile forms have used since batch 20 counted any error as "the server refused", which would have hidden a defect; it is now one helper in the app package that handles the typed failure and lets anything else surface, and the one write that still threw an untyped error (revoking a consent that is not active) is typed. The password form keeps the old rule until the next batch.

  • The lead, 2026-11-11 — the page count, reconciled. The plan's statuses ran one low since batch 21 (they said 79 after batch 23; 80 is right). Against the page-code map: 88 pages, of which 84 are on their boards after batch 24. The four left: the web share pages (L0-07; not blocked, next), and 시작, 이메일 가입 and 연결된 계정, which wait for W5 by the owner's word.

  • Owner, 2026-11-01 and 2026-11-10 — the flat v2 build, W4 batch 25: 웹 공유 페이지 (L0-07), the pages a share link opens in a browser (record: docs/history/rebuild-log.md "Flat v2 build, batch 40"). The lead's calls, for veto: (1) A web share page never says who, as in the app: no host, no roster, no faces. The profile page shows the name and no initials. (2) The session page stops showing the stored title, the free-text place and a map; it shows the derived title, the venue's name or its district, and the board's rows. (3) A shared result reads as in the app: 검토 중 for an open review, 기록 제외 with no numerals for a voided match; the word 분쟁 left the page and the link card. (4) The profile page shows 복식 ELO, NTRP, 신뢰 점수 and 기준일. The score is a number only, hidden until five recorded sessions, never the record behind it. An anonymous visitor holding a profile link could already read the name, the doubles ELO and the trust tier, and there is no per-person setting for those; the score follows the same rule because the board draws it. Told to the owner on 2026-11-11 as theirs to veto. (5) The club page shows the club's row with its level band and join method and a mono initial, no logo. (6) The read gained only the keys these rows need (migration 00740), additive, with a database test that holds each kind's payload to a list of allowed keys. (7) 앱에서 열기 is a plain link to the app on every platform; the install path waits for the store links. (8) The link card is the board's ink card (the lockup and the page's own lines; no map, no chips, no trust tier, no region); the card's lines, the page's title and the image's description come from one function. (9) A match's finish on the card is Seoul time, and the page's rows are drawn in the browser, so the day never differs between the server and the visitor's device. (10) Share pages are not indexed and are rendered on request. (11) A failed read is an error page, never "not found". (12) The recruitment Tag is derived, so six of eight seats reads 마감 임박 where the board drew 모집 중; the board is corrected. (13) Joining a club from a link while signed out is not in this batch: it needs a new anonymous, rate-limited read; owed. (14) The browser gets only what the page draws. The page's data is written into the document for the client part, so the stored title, the free-text place, the coordinates, a match's session title and a profile's photo and raw tier are removed on the server first (previewForBrowserPage, with a contract test on the built page). (15) The page's content is drawn in the browser, not in the server's HTML: the web shell holds every route's content until the browser has measured (so does /login), which predates this batch. The server's part is the metadata, the link card and the not-indexed mark; a missing target answers 200 with that mark, not 404. Drawing the shell on the server is owed as its own change, since it touches every web route.

  • Owner, 2026-11-10 (decisions 1 and 11) — follow-up batch F1a: the club on 공개 프로필, and 클럽 알림 in 클럽 홈 (record: docs/history/rebuild-log.md "Flat v2 build, batch 41"). The owner's decisions applied: the person block's club is a plain fact again (batch 21a's link inside the block is reverted in full) and a separate row under it opens 클럽 홈; 클럽 홈's ⋯ menu gets 클럽 알림. The lead's calls inside them, for veto: (1) The row is one Cell 클럽 with the club's name, shown to anyone who can read the person's first club (a club-mate, a friend, a visitor of a public club); not for a private club the viewer cannot read, not when the person has no club, never on the viewer's own page. This supersedes owner review 51's "no club Cell under the block" for this one door. (2) 클럽 알림 opens in place as the Sheet 알림 설정 already uses (전체 / 공지만 / 끔), from the member's ⋯ and from the officials' 관리 sheet; one Sheet serves both pages. ⋯ now shows for every member; 신고 stays out of it. (3) A failed change says so in the Sheet, which stays open; offline the row is disabled.

  • Owner, 2026-11-10 (decision 8) — follow-up batch F3a: the person who entered a score may correct it for 10 minutes (record: docs/history/rebuild-log.md "Flat v2 build, batch 42"). The owner's decision applied: no 8-second undo; the entrant corrects for 10 minutes. The lead's calls inside it, for veto: (1) A score is rated at once; a correction replays the ratings. No "rating pending" state. (2) The 10 minutes are enforced on the server; the phone's clock only shows or hides the door. (3) Only the person who entered the score (recorded from this migration on; nobody self-corrects an older score); a host who entered it for others is that person. (4) A correction replaces a played score with a played score; a retirement, default, suspension, draw or walkover keeps 점수 검토 요청. (5) The door 점수 고치기 shows to the entrant for 10 minutes where everyone else sees 점수 검토 요청; the Sheet is the score entry, prefilled; the saved Toast has no 되돌리기. (6) A closed window, a score someone else changed and an open review are answers, each a Dialog over the Sheet, never an error. (7) The other players are told when a score they were shown changes (one notice each, under their rating notification setting). (8) At most 10 corrections an hour per person. (9) Found and fixed: a rating replay re-sent every rating notice as new and unread (all 3,278 on the local database after one replay). A replay is now silent. (10) Found, proven and fixed (migration 00743): a rating replay deleted every self-assessed starting rating, on every score correction, review resolution, exclusion and reopening; a seeded player who had not played lost the rating, one who had was recalculated from the default start. The replay now starts each player from their recorded start; a stale demotion shield no longer leaks into a replay. One stated limit: a self-assessed mixed-doubles start that had already played before this fix cannot be told from a derived one and is re-derived. (11) Each correction replays every rating ever recorded (8.2 s for 1,786 matches locally, linear; other score entries wait behind it). Ruled: a replay from the corrected match forward, proven equal to the full replay by a database test, is built as follow-up F3c before the release gates. Rating a match only after the 10 minutes was rejected: it breaks "rated at once" and decision 9's "right after the match". (12) A match recorded as one number per side (the old form) has no correction door: the Sheet edits sets; such a match keeps 점수 검토 요청. (13) A typed refusal is a line in the Sheet; anything else fails loudly and is reported, never shown as a saved score.

  • Owner, 2026-11-10 (decision 9) — follow-up batch F1b: the level-up celebration (record: docs/history/rebuild-log.md "Flat v2 build, batch 43"). The owner's decision applied: on the player's own scorecard right after the rated match, and on 기록 if not yet seen. The lead's calls inside it, for veto: (1) A level-up is the NTRP numeral the app prints going up (doubles), not a tier name; the tier's inbox notice is unchanged. (2) It shows at once, not after the 10-minute correction window (the owner's "right after"); a correction inside those minutes can take back a level already shown; it is never shown twice. Told to the owner on 2026-11-11. (3) "Seen" is one mark per person on the server, so it holds across devices and survives a rating replay; it only rises. (4) The mark starts at the person's starting level (the self-assessed start, or the default), so a 2.5 beginner reaching 3.0 is celebrated; this needed the numeral rule mirrored in SQL, held by a parity test (it replaces the earlier "no SQL mirror" line). (5) A climb that the newest match no longer holds is not celebrated. (6) The Dialog reads "NTRP 3.5 달성", 다음에 | 공유 (the app's word for later); on 기록 a Notice with 보기 opens it; either button marks it seen; a failed mark is silent and asked again at the next view. (7) A mixed-doubles match never moves the doubles level (confirmed in the rating functions). (8) The unmounted tier sheet and its detection hook are retired. (9) 공유 sends the KakaoTalk card the tier sheet used (the registered template is unchanged): its title line carries "NTRP 3.5 달성" and its figure the doubles ELO. A template of its own waits for the Kakao console (W5).

  • Owner, 2026-11-10 (decision 16) — follow-up batch F1c: offline (record: docs/history/rebuild-log.md "Flat v2 build, batch 44"). The owner's decision applied: cached content shows under the offline strip everywhere, with actions disabled; private reads stay hidden; the 2026-06-01 rule that blanked a warm screen is replaced. The lead's calls inside it, for veto: (1) One strip, the app's, at the root of the app and of the web app; a page never draws one. (2) A warm page shows, a cold page says it is offline. (3) The kit disables write controls offline in one place; a control is drawn disabled, never removed; navigation, retry and share are never held. A wizard's 다음 / 이전 works offline; only a final step that writes is held. (4) A write that does not queue refuses at once offline instead of waiting and replaying an old intent on reconnect. (5) What keeps working offline is exactly what already queued by design: 참석 / 불참 and its cancel, starting a session, score entry, a session fee's 입금 확인 and 면제, a dues row's 입금 확인 / 면제 / 메모, manner tags, friend requests (send, accept, decline), messages and photos, reactions and poll votes, the notification family switches and marking a notice read, club set-up, attendance, read marks. The lead told the owner "score, RSVP, attendance, dues" on 2026-11-11 morning; the true list is this longer one. No write changed class; in 알림 설정 the master switch, 일정, 메시지 내용 and the club levels do not queue and are held. (6) Hidden offline, and never kept on the device's disk: 신고 관리 and 가입 심사 whole; in 멤버 the 가입 신청 queue; in 회비 관리 the 확인 대기 queue (the dues roster stays); 입금 확인 counts on the gear, the manage doors and a session's manage door; ownership claims, certifications and the contribution inbox. What a player owes is their own data: shown, no buttons. (7) 로그아웃 works offline: when the server cannot be reached the app signs out on this device only. Offline the app forgets the session on the device itself, without asking the server; online, a sign-out the server could not be reached for ends the same way, and any other failure still shows. Found on the way: with an expired session and no signal the sign-in library reports a failure and keeps the session, so trusting its answer would have left the person signed in; a test against the real library holds this. (8) Recorded gap: a cached money, attendance or score list does not say when it was read; the kit has no drawn piece for it on a list. (9) The strip also shows over sign-in and onboarding (it moved above the sign-in gate), so the sign-in page lost its own. (10) Swiping a notification away is held offline like every other write control. (11) A view count that could not be sent is dropped, not replayed later (it would be counted at the wrong time). (12) An offline refusal is not a crash: it reaches the page's own failure line where one exists, shows no Toast, and is not reported as a defect. (13) The ownership-claims queue and the contribution inbox are withheld offline even when this visit already read them.

  • The lead, 2026-11-11 — follow-up batch F3c: a score correction replays the ratings from the corrected match forward (record: docs/history/rebuild-log.md "Flat v2 build, batch 45"; a ruling of batch F3a, call 11). The lead's calls, for veto: (1) A correction, an exclusion, a reopening and an agreed correction recalculate only the matches at or after the changed one; the full recalculation stays as the repair tool and as the proof. (2) The fast path is used only when it can be proven equal; eight named conditions fall back to the full recalculation (a history row without its stored before-state, tied order, history of a match that no longer counts, a late confidence bonus, a broken rating chain, a gap in the match count, a mixed-doubles start of unknown origin, a ledger that would go below zero), each with a test. A database test compares the two paths table by table on nine cases. (3) What the fast path needs is kept in a private table no client can read, not on the rating history the club can read. (4) The migration recalculates nothing by itself, so a schema push never rewrites the ratings; the first correction after it goes live pays one full recalculation, which fills what the fast path needs. The owner may run the full recalculation once, deliberately, before launch so the first real correction is already fast. (5) A tie between two home clubs goes to the lower club id (the full recalculation had no tie-break). (6) Stated limit: a difference before the corrected match that reaches a later player only through other players' state is not detected; a world built by the full recalculation or by live scoring in order has none. Measured locally: 73 ms against 11.5 s for the newest of 1,787 matches.

  • Owner, 2026-11-10 (decisions 12, 13, 15) — follow-up batch F4: reports reach a club's officials; a block ends a friendship; the manage counts (record: docs/history/rebuild-log.md "Flat v2 build, batch 46"). The owner's decisions applied: a report about a post, a comment, a session or a chat message also reaches the club's officials; blocking someone ends the friendship, silently; 일정 관리 shows 입금 확인 N건 and N경기 from one read. The lead's calls inside them, for veto: (1) A report names what it is about; the server finds the club from the content and checks the reported person wrote or hosts it; a mismatch goes to the platform only. (2) Only an active member's report reaches the club's queue; an outsider's stays with the platform. (3) An official sees the report and the content, then decides about the person (기각 / 확정), nothing else; the content line and its door are in the report's Sheet, the row keeps one tap; deleted content reads as deleted with no door; a chat message has no door. (4) No excerpt of the content is stored. (5) Who filed a report stays closed to officials (a database test holds it). (6) An official never sees or decides a report about themselves; if they are the club's only official, it waits for the platform. (7) Installed builds keep working (the older call stays). (8) A block deletes the pair's friendship or pending request in the same step and tells nobody; unblocking restores nothing; either may ask again. (9) The block Dialog says so only when the two are friends ("친구도 끊겨요" to two strangers would be false). (10) The unused second way to block (a friendship marked blocked) is closed; pairs already friends and blocked are cleaned by the migration (none on the local database). (11) The fee count follows exactly who may confirm a payment (the host or the club's dues authority) and never counts the viewer's own transfer; everyone else gets nothing, not zero; it is a staff figure, so it is not kept on the device and not drawn offline. (12) The session page's 입금 확인 Notice follows the same authority (it was host only). (13) N경기 counts finished matches that count toward the record; it travels with the fee count in one read, so it is not drawn on a cold offline start. (14) A failed report or block is shown where the person acted, never as a Toast.

  • Owner, 2026-11-10 (decision 10) — follow-up batch F3b: 대진 짜기's 진출 인원, and choosing who plays (record: docs/history/rebuild-log.md "Flat v2 build, batch 47"). The owner's decision applied. The lead's calls inside it, for veto: (1) 진출 인원 is 1 or 2; three would need byes, which nothing draws or pairs: not built; the board's stepper is 1 to 2. (2) One group advances two; two or more groups advance one or two; never as many as the smallest group holds. The server refuses any shape it could not pair (a direct call could store one before); a single group of two teams is refused with it. (3) With one advancer the winners meet across neighbouring groups in order (A–B, C–D); the two-advancer pairing is unchanged, proven against the earlier pairs. One table of cases holds the server and the app together. (4) Choosing who plays is the 참석 row's choice (참석자 전원 / 선택한 사람만) and then a people picker ending in "N명 선택" (the app's wording rule refused the board's "N명으로 정하기"; the board follows). It has its own write, open to exactly who may generate the draw (the general session edit posts to the club board and admits club admins only), which changed the brief's "no schema" (migration 00749). (5) The picked list is fixed: a picked person who cancels drops out, a later RSVP is not added, the next round reuses the list. (6) One people picker in the kit serves the draw and the group chat (the chat's own picker moved onto it). (7) Choosing who plays needs a connection.

  • Owner, 2026-11-10 (decisions 4 and 5) — follow-up batch F2, cut 1: a dues plan 한 번; the 장부's date, opening balance, receipt and download (record: docs/history/rebuild-log.md "Flat v2 build, batch 48"). The owner's decisions applied: a one-off charge with a name and a due date (일정마다 is not built); 지출 추가 gets a date and the 장부 an opening balance, then the receipt photo and the download. The lead's calls inside them, for veto: (1) Saving a one-off charges the members at once (the hourly job is only the backstop); the page says how many are charged before the press. (2) A member who joins while the due date is still ahead is charged by the server; one who joins after owes nothing; an official may waive a row as before. (3) A one-off that already has charges cannot change its amount or dates (the server refuses it; the charges are money evidence): it can be renamed or stopped (중단, the charges stay) and a new one made. (4) 회비 관리 chooses among charges: the recurring plan's months, then each one-off by name. (5) Choosing 한 번 on 회비 설정 starts a new one-off and leaves the recurring plan untouched; 일정마다 left the boards. (6) A member with a monthly charge and a one-off both open pays the head charge first, as the app did before this batch; left unchanged, the owner's to change. (7) The 장부's date is never after today (the server refuses it). (8) The opening balance is one line with a date; officials can change or remove it, and the Dialog says by how much the balance moves. (9) The balance counts from the opening's date (matrix in the batch brief): with an opening, the balance is the opening plus every entry dated on or after it; older entries stay in the feed under one head, 잔액에 들어가지 않는 기록. Counting everything, as before, counted twice whatever the opening already contained. The summary, the feed and the download give one figure (a database test holds them). (10) The receipt is for officials only, re-encoded and made smaller before it is sent; a receipt whose entry was refused is removed again. (11) The download is a file to save, never an import, and reads 장부 내려받기 (내보내기 means removing a member); 이번 달 / 올해 / 전체. (12) The 장부 shows cached content offline like 회비 관리's list; a receipt's link is never kept on the device. (13) The download works on phones through a file-share module added to the app (it needs the W5 native build and has not run on a device); on the web it is a file download. (14) A receipt comes from the album; taking one with the camera needs a native permission and waits for W5. (15) Creating a one-off is safe to retry: the app names the new plan before the first save, so a retry after a lost answer lands on the same plan and nobody is charged twice. (16) A stopped one-off keeps its name on the charges that remain. (17) Found on the way and fixed for every upload: a photo sent from the web app kept its location data; the web path now re-encodes it as the phone path does.

  • Owner, 2026-11-10 (decisions 6 and 7) — follow-up batch F2, cut 2: the refund on a self-cancel; a 매치's own account (record: docs/history/rebuild-log.md "Flat v2 build, batch 49"). The owner's decisions applied: a paid player who cancels at least 24 hours before the start opens a refund, and a later cancel keeps the fee unless the host chooses otherwise; for a 매치 the host enters an account for that 매치, shown only to people holding a seat. The lead's calls inside them, for veto: (1) The server's clock decides, at the cancel. The time the phone sends is never used; one constant holds the 24 hours on the server and in the app. (2) A transfer the host confirms after the cancel is decided by what was true at the cancel (recorded on the payment at that moment), not by the confirmation's hour. (3) The host's exception (a refund for a late canceller) is open to exactly who may confirm a payment (the host or the club's dues authority), never the payer; its command reads 환불 대기로 바꾸기 (the app's wording rule refuses 환불하기). (4) A player the host removes is refunded whatever the hour. (5) A player whose refund has opened cannot re-join that session, by any path; a late canceller who kept the fee may re-join on the payment that still stands. (6) No host "refunded" button: the payer's 환불받았어요 is the witness, as before. (7) Manual transfers only: a PortOne payment opens the same obligation, and settling it through PortOne waits for the payment rail. (8) The cancel Dialog says what happens to the fee and until exactly when (a date and a time, never "24시간"); every board line that said a 2-hour free cancel is corrected. (9) A member who leaves a club has paid future seats refunded by the same rule (only with the 24 hours left); a member removed from a club is refunded always; a seat that lapses by itself keeps the fee as before. (10) A 매치's account is its own row per session, never on the session (an open 매치 is publicly readable) and never the host's one account for all their matches. (11) Only the host writes it; only the host and a confirmed seat read it (a seat still inside its payment window counts); the waitlist, a cancelled seat, an outsider and a signed-out visitor get nothing, and their app never asks. (12) A series copies the account to each occurrence. (13) It is deleted with the session, at once when the session is cancelled, and 14 days after the session ended (30 at the latest while a reported transfer is unconfirmed). (14) A changed account tells the players who already reported a transfer, without the number. (15) An account number is never kept on the device's disk; this now also holds for a club's account, which was being kept. (16) A paid 매치 can be created without an account; the host sees a Notice and adds it on 일정 수정. (17) The line 참석이 확정된 분께만 보여요 stands under the account, a deliberate exception to the no-note rule, because it tells the host who can see a bank account.

  • Owner, 2026-11-10 ("the recommended direction for all open decisions") — follow-up batch E: 사유 인정 요청 (record: docs/history/rebuild-log.md "Flat v2 build, batch 50"). The owner's decisions applied (the research's fourteen recommendations): a category (아픔, 부상 / 가족 일 / 업무 / 교통, 날씨 / 기타) and an optional note of up to 100 characters; the decider sees how often this person was excused in this club only, counting only requests an official accepted; the answer changes the record at once and the requester is told after a 10-minute undo window; only the answerer may undo; a 거절 is final; a direct 사유 인정 has the same undo; for a 매치 its creator answers, with the owner or admins of its club; a request nobody could answer is refused; seven days from the session's date; the category and the note are treated as health information; the 3-day reminder is its own notice; dismissing the ask at cancel time loses nothing; re-joining leaves an accepted excuse as it is. The lead's calls inside them (the owner agreed on 2026-11-12): (1) Nobody decides their own record, by any role: the old direct 사유 인정 let an owner who did not show up excuse themselves; it now refuses. (2) One authority rule serves the request-time check, the answer, the undo, what the record shows, who is notified and 홈's count. (3) The count comes from the requests, never from excused records (the system writes those for host cancels and expired holds too). (4) The undo restores exactly what was there and re-opens the notices the accept closed, sending nothing. (5) Inside the 10 minutes the requester still reads 확인 중, whatever the answer, although the figures already changed: nothing shown is ever taken back. (6) A change of trust tier caused by an accept is not announced inside the window. (7) The ask after a cancel opens only after the cancel succeeded online and only when a record exists that can be asked about. (8) Offline: the three writes are refused; the officials' waiting requests are not kept on the device; the requester's own request is. (9) Retention: a request is deleted when its record leaves the 12 months and with the person's withdrawal; it is in the personal data export. (10) In the app an official sees who asked; a push names no one; neither carries the category or the note. (11) 홈's 사유 인정 card opens the oldest waiting request's member record. (12) The category words use commas (아픔, 부상; 교통, 날씨): the app's wording rule refuses the middle dot. (13) The purpose is recorded as sensitive with consent as its basis, and no separate consent is collected yet: the legal check before launch is the owner's to arrange (release list). (14) A 매치's creator who is not an owner or admin of its club is notified and can answer on the server, but has no page to answer from (recorded gap; in practice a 매치's creator owns its club). (15) An official can still excuse a record that was not an absence (unchanged behaviour of the direct 사유 인정).

  • Owner, 2026-11-12 — "Agreed on all open decisions." The calls put before the owner that day stand as the owner's decisions: leaving a club refunds paid future seats only with the 24 hours left and a removal refunds always; a 매치's account is replaced or goes with the fee, with no separate 지우기 (release list S23 stays as a known limit); nobody can excuse their own record; inside the 10-minute undo window the requester still reads 확인 중; an official sees who asked in the app and a push names no one; the legal check on 아픔, 부상 as health information is the owner's to arrange before launch, and no separate consent is collected until then. The other calls listed in the entries for F2 cut 2 and batch E were before the owner in the decision log on the same day and are covered by the same word.

  • Owner, 2026-11-10 ("Results should be settled from matches") — follow-up batch C: the 클럽 대항전 loop (record: docs/history/rebuild-log.md "Flat v2 build, batch 51"). The owner's decision applied: a challenge's result is counted from the matches played in sessions tagged to it, never typed in. The lead's calls inside it (the owner took the recommendations on 2026-11-12): (1) A hole was closed: an owner or admin of a club could insert, through the data API, a challenge that was already accepted, with scores and a winner, against any club and in anyone's name. No client can insert, change or delete a challenge now; four server functions write it (propose, answer, cancel, settle). (2) A match counts when it is completed, recorded, not friendly, in a session tagged to the challenge and dated inside its period, with every player of one side in one club only and every player of the other side in the other club only. (3) The score line is always the match tally; the rule decides the winner; a tie is a draw. (4) Only 총 승수 and ELO 상승 can be proposed; old rows keep their stored rule (승률 scores as 총 승수; 경기 수 reads 출전 인원). (5) The standing is counted on read while the challenge runs and frozen once, 7 days after its end date (the score-review window); a challenge with no counted match ends 0-0 with no winner. (6) Proposing starts on the 대항전 page and needs the plan; receiving and answering never do. (7) One open challenge per pair of clubs, at most 5 proposals a day per club, a period of at most 90 days. (8) Withdraw and cancel: the proposing club withdraws a proposal; either club cancels an accepted challenge until a tagged session has a completed match. (9) A session is tagged in 경기 유형, only to a running challenge whose period holds the session's date; the server validates it; an old 대항전 label with no tag stays valid. (10) A tagged session admits the other club's members without becoming public: they see it, apply through the existing application path (capacity, the level band, the cooldown, the fee and the refund rule apply as to anyone), see their own seat and the matches they play in, and keep the session after the challenge ends. They do not see the host club's roster. (11) The host club's bank account is shown to the other club's member only while they hold a confirmed seat in a paid tagged session, up to 14 days after it; never to the waitlist. (12) Notices: a proposal reaches the invited club's officials; the answer and a cancellation reach the other club's officials; a tagged session reaches the other club's officials; the settled result reaches both clubs' members once. A push names no person. (13) 철회 and 취소 are a button under their row (the row itself opens the club it names). (14) The QA world's seeded challenge, stored with a typed-in 1-1, is settled from its real matches by the job's first run on hosted (0-0, no winner, one notice to its members). (15) A proposal past its end date holds the pair until it is withdrawn or declined (no automatic expiry in this batch). (16) The other club's member does not see the live score call strip (members only). (17) A tagged session approves the other club's members automatically; a manual choice for the host is not built. (18) A session scheduled before the challenge was accepted can be tagged later in 일정 수정, and untagged while it has no completed match. (19) The proposal form is its own page (/clubs/[clubId]/challenges/new), closed to anyone the button is closed to. (20) A person can always read their own RSVP row and the matches they play in (a general rule added with this batch).

  • Owner, 2026-11-12 — an engineering pass before the new build, and "I would go with recommended practices with all open questions." After the implementation run the codebase gets a thorough engineering pass (naming, structure, error handling, efficiency, security), then a review with the owner, then the owner pushes the build. Two read-only audits were made the same day and the plan is six behaviour-preserving waves. The owner took the recommendation on each open question: (1) a withdrawn person's sign-in record is scrubbed in place when the profile is anonymized at 30 days (a placeholder address, no phone, linked identities and metadata removed; the id stays so other people's match history keeps its integrity), their private notes and feedback notes are deleted, and their posts, comments and messages stay under the anonymized id unless the legal check says otherwise; (2) the developer account and the developer functions are off at launch (one gate is built now; the switch and the account on the production project are turned off at launch, a release-list item); (3) the preview build's shared auto-fill test password is removed from the build profile. The same word covers the calls put before the owner that day for the 대항전 batch and the release-list batch (the share preview's derived title; a signed-out visitor seeing which club an invite link is for, behind a daily cap; 철회 and 취소 as a button under their row).

  • The lead, 2026-11-12 — release-list batch R: the "Owed Before Launch" items that need no device (record: docs/history/rebuild-log.md "Flat v2 build, batch 52"; the owner took the recommendations on its open calls the same day). The calls: (1) The board lint joins the gate with zero problems: eleven of its thirteen recorded problems were board defects and are fixed; two keep a reasoned exception. (2) 대진 짜기 holds a group stage the server would refuse (two teams; 7, 13, 14 and 15 teams at a group size of three) before the press, from one rule. (3) The public share preview never returns a stored title, a typed place or coordinates; a session with a typed place and no linked venue shows its derived title on the share page. (4) A signed-out visitor sees which club an invite code is for, limited to what a discoverable club already shows publicly, behind a cap of 2,000 a day for all visitors together; when the cap is spent the page is the one before this batch. (5) Guessing invite codes is throttled: 15 lookups and 5 joins an hour per person. (6) A join by code works only for a club the person already found through the lookup (a refusal rolls back its own charge, so a failed join could never be counted); without that the join answers exactly as for a wrong code. A club page opened with a code in its address looks the code up first. (7) An invite code is treated as a secret: never kept on the device's disk, never in an error report. (8) Three privileged functions that checked nothing about the caller are closed to clients (every user's client error rows; the shared daily map and search budget; marking another person's notice resolved); nothing in the app called them. (9) A score-correction replay sends no notices: already true; a test now pins it. (10) The web shell drawing nothing on the server stays on the list as a later improvement (share pages are not indexed and their link cards already render on the server). (11) The operator band no longer shows a helper line when there is no draw (the page already says so; the line after a refused press stays).

  • Owner, 2026-11-12 — the engineering pass, wave 1: the safety net, telemetry, the server's grants and bounds, the cache as an allow-list (record: docs/history/rebuild-log.md "Flat v2 build, batch 53"; the plan and both audits are kept with the hand-over notes; the owner's three decisions of the same day are applied here and in wave 2). The lead's calls, for the review: (1) Refactoring waves preserve behaviour except for changes they name; every earlier test passes unchanged, and tests that pin today's behaviour land before the code they protect. (2) Error reports carry no personal data: one redaction in front of every sink; the database's detail text is no longer part of a message. (3) The on-device query cache writes only what a table allows, each row with its reason; a new query is not written to disk until someone decides it should be. (4) Found: the disk cache never survives a restart (every cold start deletes it, since 2026-09-07, so one account's data can never show to another). This pass ships the allow-list and corrects the canon; the native build carries expo-crypto; an account-stamped, encrypted cache that restores for the same account is built after the build has passed device QA, never in the first build after a long pause. (5) The shared test password is out of the preview build profile (the owner's decision 3). (6) The developer functions need both the developer account and the switch (the owner's decision 2: the switch off and the account disabled on the production project at launch is on the release list). (7) Anonymous telemetry writes are bounded in size and rate and cannot forge a watchdog alert; an attacker can still spend a day's global cap and stop error reports for that day (the same trade-off the performance events already have). (8) Found by the first tests: a network failure can sign a person out (a failed session renewal is treated the same whether the network or the sign-in service refused); fixed in wave 2 as a named change.

  • Owner, 2026-11-12 — the engineering pass, wave 2: failures become visible; rules take their inputs (record: docs/history/rebuild-log.md "Flat v2 build, batch 54"). The owner's decision applied: a withdrawn person's sign-in record is scrubbed in place when the profile is anonymized at 30 days; private notes are deleted and feedback notes cleared; posts, comments and messages stay under the anonymized id unless the legal check says otherwise. The lead's calls, for the review: (1) A network failure never signs a person out. A failed session renewal was treated the same whether the network or the sign-in service refused; now only a refusal by the sign-in service signs out, and no network, a timeout, a server error and a rate limit keep the session and are retried each minute. A refusal on an expired session signs out at once. (2) The confirm Sheet no longer swallows a failed action (every consequential action goes through it). (3) Keys for writes that must not repeat come from one helper on a secure random source (expo-crypto, added for the build), never Math.random(). (4) On the server a swallowed error leaves a record (a SQLSTATE and an object name, never a row's values), and no block discards silently. (5) The cron watchdog reports a failed run on that run and a job that has not run within its expected interval (it used to count its own ticks, and never noticed a job that stopped). (6) Rules take time and randomness as parameters; no rule function is nested deeper than three levels; every engine's output was proven identical under random inputs before and after. (7) The scrub's first run on hosted will scrub whoever withdrew more than 30 days ago on the QA data; the migration itself rewrites no row.

  • Owner, 2026-11-12 — the engineering pass, waves 3 and 4: trust at the boundary; a failed write shows on its page (record: docs/history/rebuild-log.md "Flat v2 build, batch 55"). The lead's calls, for the review: (1) A failed write is worded by the app, never by the server: one reader maps a failure's hint, then its code, to the app's words; the server's sentence is never shown. (2) Every write names where its failure shows, and an error toast is no longer the default: a helper line on a form or a band, a dialog for a page whose write has no form, a notice on the page. (3) A background write the person cannot redo (marking something read, dismissing a notice, a clean-up when leaving a page) shows nothing and is reported. (4) Server data is validated where it was trusted: the home, club and session bundles are checked against schemas that report a mismatch and never block a screen; enforcing them waits until a release has run clean. (5) A failed read is not an empty answer: the weather adapter and its siblings report a failed read instead of returning nothing. (6) One membership rule in the policies (an active member), and the caller's identity is read once per query, not once per row. (7) Lookups that can be enumerated are capped and limited: profile search needs two characters and allows 40 searches in 10 minutes; the club name check allows 30 an hour and refuses instead of answering "no match"; the join-request probe answers only for oneself or a club official; season standings are for active members. (8) A ratchet in the gate fails on a new error toast or a newly shown server message.

  • Owner, 2026-11-12 and 2026-11-13 — the engineering pass, wave 5 (the last before the build): precision and homes (record: docs/history/rebuild-log.md "Flat v2 build, batch 56"). The owner's words of 2026-11-13: after all the planned work, the rest of the app, front and back (database, network, tests, security, typography, colours), is brought to the same standard so the app is ready for visual and functional tests: a readiness pass follows the UI/UX pass. The lead's calls, for the review: (1) A write refreshes exactly what it changed where that can be enumerated; where it cannot, the wider refresh stays (a stale screen is worse than an extra request). (2) A list reads once for all its rows. (3) Rules live in the domain layer, orchestration in use cases, I/O in adapters; names changed only where a name misled; no exported name, route, word key or server object was renamed. (4) A link with a malformed id shows not-found and sends nothing. (5) Eighteen write hooks no page used are deleted; their server side stays and is listed for the owner as features with a back end and no screen (the live score call, score-correction proposals, the referee hand-over, venue corrections, membership recommendations, regular meets in bulk, starting the next round in bulk). (6) The schema has a generated snapshot instead of a squash of the 756 migration files. (7) Money columns refuse a negative value; on a database whose old rows cannot be read first, a check that an old row fails is dropped and recorded instead of aborting the migration or leaving a row that can no longer be updated. (8) Write privileges no policy backed are revoked. (9) The sixth wave (the sign-in lifecycle and the create-club flow decomposed) is not run before the build: it needs time to soak; its characterization tests are in place.

  • Lead, 2026-11-13 — the security review and its closure (migration 00765) (record: docs/history/rebuild-log.md "Flat v2 build, batch 56"). An adversarial review run for the readiness pass proved four high-severity authority holes on the local database; the lead closed them in the same batch as the engineering wave instead of waiting for the readiness pass, because the hosted test project carries the same rules. Calls, each open to the owner's veto: (1) a person becomes a club member only by their own act: the direct insert is gone, and the two function paths that made a person a member without their agreement (an official accepting a nomination; the guest conversion) now end in an offer the person accepts or declines; the earlier ruling "총무 확정, 평점은 근거" stands for who is offered; the person's side has no screen until the next batch; (2) key columns are immutable to clients through one guard, not column revokes, because the app's upserts name their keys and a revoke would refuse them; (3) entitlements, challenge contributions and set scores are written only by privileged functions; the client write methods no page called are deleted; (4) the anonymous role reads an allow-list of columns on sessions and club media (host ids, pools and costs are not on it); (5) deferred with reasons: profile-field privacy (primary club, attendance rate: ruled private by default, built in the readiness pass with the client reads); the developer password in the development build profile (the owner rotates the account on hosted; the profile entry leaves with the tester sign-in change); code-signed updates (the owner's key, in the W5 build); enforcing the web content security policy (after a reporting window); a per-minute cap on rating replays (before launch scale); (6) a card cancellation is heard and logged but does not release its hold until a database rule exists (owed before the card rail goes live; the lane did not invent a money rule); (7) after three read-only reviews of the batch, before it was committed: an offer that ends without a yes returns the guest to an ordinary guest, removing a guest ends their offer, and the counts an official sees (가입 N, 할 일) leave out offers they cannot decide; (8) the words: the offer is 가입 제안, answered with 수락 or 거절, withdrawn with 제안 취소 (in the terminology guide); (9) a post reaction can be switched, and only a member of the post's club reacts (found while checking grants: no update rule existed, and the insert rule checked only the person's own id); (10) the seed function's service key is a credential by itself; the allow-list gates people, so the local QA path needs no environment edit and every gate the hosted path had stays.

  • Lead, 2026-11-13 — the UI/UX pass, wave A and the first half of wave B (record: docs/history/rebuild-log.md "Flat v2 build, batch 57"). Calls, each open to the owner's veto: (1) a membership offer is answered on the club's page, not in a notice or a row: the row says the offer and opens the page; the band holds 수락 with a quiet 거절 (joining always happens on that band); (2) the offered club leads 내 클럽 and its Tag is a task (the viewer's move), while the viewer's own request is waiting; (3) officials are told of a decline but not again of an accept (the member-joined notice already says it); an official may offer the same person three times a week per club; (4) one word, one meaning: 가입 제안 is the offer; the guest shortlist is 가입 후보; 가입 제안 보내기 sends; 제안 취소 withdraws; (5) the clubs tab follows its board, with two kept differences: an official's waiting work stays on the row (the board gains the frame), and the filter sheet's primary says 적용 because results are paged on the server; (6) the FAB is the one home of 클럽 만들기 and treats offline as the sessions feed's FAB does; (7) no refresh control on the web (the boards draw none; lists refetch when the tab returns); (8) the share cards keep the ratified full ink Block and the root card is the white canvas with the block: the brand document names both grammars; (9) where a page's own order of states differed from the offline law's, the law's order wins (offline over a failed read over loading); (10) Consent Management is hidden offline (a stale copy of a legal choice must not look current) and joins the law's list; (12) an offer ends by one rule and every end closes it (an accept, a decline, a cancel, the guest's removal, joining another way, the club's archive); a cancelled offer sends the person nothing (they never applied); a full club's offer waits with 거절 only; (13) a member's own receipts and claims stay readable offline; the officials' view of others is hidden (transfer proposals are others' evidence and stay hidden); (14) the member row follows its board: the viewer's answer in the who row, an official's total waiting work in the Tag, the next 일정 as a fact (the old card's two tappable sessions are gone: one door per row), the unread count from the existing bulk read; (15) the region filter picks one district (the query takes one); (16) boards that had already drifted from their code are not re-accepted in bulk: they wait for the rendered check; (11) emoji stay on the tier badges (the owner's earlier decision recorded on the trust badge), and reactions and medals keep theirs.

  • Lead, 2026-11-13 — the UI/UX pass (the rest) and the readiness pass, wave 1 (record: docs/history/rebuild-log.md "Flat v2 build, batch 58"). The owner's words of 2026-11-13: the blocking items are handled "in the evidence-based, recommended way"; "Don't worry about the cockpit login. We will work on the name once we've completed the app." Done under that word: the hosted developer account's password was rotated (the new value lives only in two untracked env files; no build profile holds a credential); the database ports were measured (closed at the Windows firewall since 2026-11-10; this machine has a public address and Docker Desktop ignores the documented localhost binding, so one firewall block rule remains the owner's optional hardening); the cockpit sends a no-index header. Set aside by the owner: the cockpit's login and the name (filing, addresses, rename), which wait until the app is complete. The lead's calls, each open to the owner's veto: (1) a write is never re-sent automatically unless keyed; a keyed write may retry once on a transport or a server failure; (2) a request that needs a signed-in person is never sent as the anonymous role: the page says it could not confirm the sign-in instead of showing an empty list; (3) three transport failures in a row mean offline, cleared by a health probe; (4) profile activity is private by default: clubmates and accepted friends see it; a stranger and a signed-out visitor do not, unless the person chose public; (5) the officials' queues and other people's money are not drawn offline; a member's own money is; (6) a dialog that asks "do X?" offers the verb and 닫기; 취소 is only ever the action; (7) 일정 수정's fee is 무료 | 유료 and a 유료 with no amount is refused; the day is picked in the calendar sheet up to three years ahead; (8) a link that will not open stays a Toast (it has no page of its own); every other failure is on its page or control; (9) no selection haptics (the behaviour addendum allows haptics for outcomes only); (10) the coachmark the hub board draws on the 관리 gear is not built (recorded as a gap); (11) the Expo packages take the table's patch versions before the build; (12) the app's retry rule is the only retry (the database client's own is switched off); (13) slow is not offline: a timeout does not count as unreachable, and one probe confirms before the app says offline; (14) the anonymous allow-list holds only answers that do not depend on the viewer; (15) a public 매치's host shows no club or attendance rate to a stranger unless the host chose public (the consequence of private by default; the owner may prefer hosts of public events to show them); (16) boards the batch did not touch are not re-accepted: thirty still differ from their grounded sources and wait for the rendered check.

  • Lead, 2026-11-14 — the readiness pass, waves 2 and 3 (record: docs/history/rebuild-log.md "Flat v2 build, batch 59"). The lead's calls, each open to the owner's veto: (1) nothing renders under 12 px at default size, the digit in a count badge included (the board kit drew 11; lifted to 12/16); (2) the medals are the component library's set in both schemes, and the dark platinum on the dark card is the contrast gate's one recorded exception until the lighter dark value adopted on 2026-11-14 is built; (3) state is ink: progress, meters, rings, selected rows, links and the focus ring are ink on a neutral track, and the ball colour is kept for the one main action, which the legacy primary Button now also draws; (4) one amber and a separate dark success fill; (5) the seed accounts' password is a function secret (SEED_ACCOUNT_PASSWORD), in no source file, and both seed functions refuse without it; (6) the fourteen phone flows are rewritten, none retired, and none is called passing until it has run on the new build; (7) three database tests are scoped to their own fixtures, because they counted rows of whatever world was loaded; (8) twenty-two boards were re-accepted by name after their frames were read against the new values, and the rest wait for the rendered check.

  • Owner, 2026-11-14 — best practice adopted for every open decision. The owner reviewed the list of 92 open decisions and said: "for all of them, I want you to review each one, find best practices for each, and adopt them as the answer." Each was researched against, in this order, the law and the stores' rules, published standards, comparable products, and the app's own specification; a fifth pass checked the legal and the weakest citations against the official pages, and two read-only passes over the server corrected four answers whose premise was out of date (no club official could already read who filed a report; only an excusal's deciders could already read its reason). Outcome: 68 kept as built, 20 changed, 4 that only the owner can do (the firewall command for the local database ports; the legal check before launch; a keyboard check on the phone after the build; the review of the rebuilt pages). The answers, their sources and the earlier rulings are on the owner's decision page; the owner vetoes by marking Change there. Rulings inside the adoption: offline keeps the dimmed button the owner decided on 2026-11-10, and the soft primary is for a missing requirement; the block rule (no profile details, no photo, no offer between a blocked pair, silently) is a deliberate choice stricter than the products found, which differ; the day-30 period for a withdrawn account is the app's own (the law says "without delay"), so it must be stated where the person consents; keeping a seat while a card dispute is open is the app's own rule and is looked at again when the card rail is built; the law was read in the official English translation, which is for reference only, so counsel's check stands. Acts on the hosted project that the adopted answers allow, once, after the last push: the test data is written and the developer functions are switched on for the test window (off at launch, with a probe on the launch checklist); the sign-in and storage settings and the server's health are read, read-only; the record page tells the owner before it happens. Changed, and when each is built:

    • Should a block between a person and an official stop an offer being made? An official cannot send an offer or a guest invitation to a person when either has blocked the other, and the refusal does not reveal the block. An offer already waiting is not touched: the person can decline it and the official can cancel it. (in the next batch, before your build)
    • Should two people who blocked each other but share a club still see each other's activity and photos? A block works inside a shared club too: when one person has blocked the other, neither sees the other's activity or photo. The block stays silent; nobody is told. (in the next batch, before your build)
    • Should club officials see a member's excusal category (아픔, 부상) and note, or only the decision? Already so on the server: only the people who decide an excusal (the club's owners and admins, and a 매치's host) can read its reason; everyone else sees only whether it was excused. What is added: before a health reason (아픔, 부상) is sent, the request asks for a separate consent, says who will see it, and offers a neutral reason with no note instead, because the privacy law treats health information as sensitive. (in the next batch, before your build)
    • What is the retention period and text policy for what a withdrawn person wrote (DMs, posts, comments, reviews)? A written rule. When a withdrawn account is scrubbed at day 30, the private messages that person sent are deleted, with their attachments; the other person keeps their own side. Club posts and comments stay, with the author shown as withdrawn, only where the text does not identify the person; the person can ask for them to be deleted when they withdraw. The rule is stated in the privacy text and on the withdrawal screen. (before launch)
    • What should happen to a card payment hold when the card provider reports a cancellation or charge-back? When the card company reports a full cancellation, the hold leaves "paid" automatically, the host is told and it is recorded. When it reports a dispute, the hold is flagged and the host is told, but nothing is refunded and the seat stays until the dispute is resolved. (with the card rail)
    • Should a sheet with typed input ask before it closes, and should unsent comment and chat drafts also be guarded? Sheets with typed input ask before discarding (already so). Comment and chat boxes do not ask; they keep the unsent text for that thread, so it is there when you return. (in the next batch, before your build)
    • Where does the "not yet" state (missing requirement or offline) show on a primary button? A main button whose step is "not yet" stays pressable and looks soft, its hint line always shows, and pressing it says what is missing. Offline is the exception you decided on 2026-11-10: the button is dimmed, with the reason beside it. (in the next batch, before your build)
    • Should 참여 in achievements and ranking copy become 참석? Use 참석 where the thing counted is being at a session ("4주 연속 참석"); keep 참여 only for involvement that is not attendance (for example taking part in a vote or club activity). Update the guide's line from 'reserved for achievements' to that rule. (in the next batch, before your build)
    • One word for a venue on forms: 장소, 테니스장 or 활동 장소? 테니스장 on every form where a venue is picked from the directory; 장소 only where the answer may be any place; 활동 장소 is retired as a form label. (in the next batch, before your build)
    • The platinum medal is too faint on the dark card: use a lighter platinum in dark mode? A lighter platinum in dark mode only: the lightest step of the same hue that passes every contrast bar (a visible mark on all three dark grounds, the white glyph on it, and the gap between earned and locked). Light mode is unchanged. (in the next batch, before your build)
    • Which ink sits behind the app icon on Android: the softer ink or the deepest ink? The softer ink sits behind the Android icon, as the brand document already says. The two inks are barely different to the eye; this makes the build agree with the brand. (in the next batch, before your build)
    • Push notices after an offline sign-out on a shared phone. Close the shared-phone gap: after an offline sign-out, remember to clear the previous account's push token as soon as the phone is online (or on the next launch), not only when the next account registers. (after the build)
    • Live score call (call, agree, reject): build a screen for it? No screen. You already replaced the call-and-agree flow with honour-system scoring. Signed-in users lose access to the old call functions now; the functions are deleted after launch. (in the next batch, before your build)
    • Score correction proposals and their agreement: build a screen for it? No screen. Corrections already have their two paths (your own correction within 10 minutes, and a review request the host decides). The old propose-and-agree functions are closed the same way. (in the next batch, before your build)
    • Referee assignment, release and hand-over: build a screen for it? No screen. Scores no longer need a referee role: a player, the host or an official enters the score. The five referee functions are closed to signed-in users now; deletion is decided after launch. (in the next batch, before your build)
    • Venue corrections (submit, instant fact fix, review, report): build a screen for it? No screen for the old venue corrections: the newer contribution and ownership-claim flow has its pages, and no page calls the two old functions. They are closed before launch, in one piece with their tests; venue reports are decided with the moderation work after launch. (before launch)
    • When a member owes a month and a one-off, which is charged first? By due date: the oldest outstanding charge is settled first, and the monthly charge wins only when both fall due the same day. (in the next batch, before your build)
    • Should 일정 만들기 ask 경기 방식 (친선 | 경쟁)? The wizard already asks 친선 or 경쟁 (the earlier note saying it does not was out of date). 친선 stays the default, and one line under the choice says the consequence: whether the session moves ratings. (in the next batch, before your build)
    • Do the three pages that wait for the native build (시작, 이메일 가입, 연결된 계정) ship in the same build as the new mark? The three pages are not in the app today and stay out of this build: 시작 and 이메일 가입 have no route and the app has no sign-up call; they arrive together with provider sign-in. Before any iOS release that offers Kakao or Google sign-in, Sign in with Apple is added (Apple's rule 4.8). (in the next batch, before your build)
    • Should a list shown from a saved copy while offline say when it was last read? Add a short 'last read' line to money, attendance and score lists when they are shown from the saved copy offline, drawn as a piece on the boards first and built after the first build. (after the build)
  • Lead, 2026-11-14 — the adopted answers, built (record: docs/history/rebuild-log.md "Flat v2 build, batch 60"). Built under the owner's instruction of 2026-11-14 above. The lead's calls inside the build, each open to the owner's veto: (1) the offer refusal checks the acting official and the person, at the moment of the offer; a waiting offer is not withdrawn by a later block (the person can decline it, the official can cancel it); (2) a blocked pair still see each other's name, rating, posts, comments and place in the roster: the rule hides profile details and the photo, and the rest is a recorded limit; (3) the consent is asked per request, only for the health reason; the rule is held on new requests, so rows written before it carry no consent moment and are never judged; (4) one word, 운영진, wherever a line names who decides an excusal (a club's owners and admins), as the terminology guide has it; (4b) 테니스장 has its own key for a single venue's row beside the page title's key: Korean has one word where English needs two, which is the case the near-duplicate allow-list exists for; (5) the consequence line under 경기 방식 sits in the field's own nested slot, so no new spacing rule was added; (6) the dues page leads with the earliest due charge, the monthly one on a tie (thin evidence, cheap to reverse); (7) two items of the adoption were moved to before launch because they are larger than the research assumed: closing the two old venue-correction functions, and deleting a withdrawn person's private messages with their attachments; (8) the rating fix is the total order only: a correction of an old match on a large world still passes the server's limit, and replaying only the affected players is owed before launch; (9) the hosted test bed is seeded with the small world, which reconciles its own ratings; the large world cannot be reconciled through the app.

  • Lead, 2026-11-14 — what the rendered check found, fixed (batch 61) (record: docs/history/rebuild-log.md "Flat v2 build, batch 61"). The lead's calls, each open to the owner's veto: (1) the password and withdrawal buttons read 변경하기 and 탈퇴하기 — corrected in the fix round: the lead's first call (the nouns 변경 and 탈퇴, because the house-style gate's 하기 list lacked the verbs) reversed the owner's table (component library § 1.19, 2026-10-25), so the gate's list now follows the table (a test reads the row) and the boards are back to the table; (2) 친구 신청 stays on the profile of a person who blocked the viewer — a block is silent; the insert is refused by trigger friend_request_block_guard (42501, hint friend_request_blocked) with the neutral line; (3) a suggestion the person may not read answers as not found — the server raises one error for absent and forbidden on purpose; the adapter maps exactly that hint to nothing and rethrows every other error; (4) a chat that cannot be opened says "지금은 이 분과 대화할 수 없어요" whoever blocked whom; (5) a negative amount prints "-5,000원"; (6) the English word files write "15,000 KRW" and never the won sign, held by lint and the house-style gate.

  • Lead, 2026-11-14 — 게스트 명단 groups (batch 61, ruling L23), open to the owner's veto. The 우선순위 명단 gets its own sub-heading ("우선순위" / "Priority", the same component and level as 가입 후보) whenever it has rows: two lists of different meaning are two groups and a group has its head, so the seam between them is heading after row, which the rhythm canon already lists (the canon is not changed).

  • Lead, 2026-11-14 — a drawing rule and a name, after the second set of pictures (batch 62) (record: docs/history/rebuild-log.md "Flat v2 build, batch 62"). The lead's two calls, each open to the owner's veto: (1) the release-build rule for drawing rules — in development and tests a broken rule throws as before; in a release build it reports once and the one component draws nothing, so one unexpected value costs a block and not the page; the tab bar and a Notice (a failed write's words and its retry) keep their throw in every build, because a page with no way on is worse than the error boundary's retry; (2) the words for a withdrawn and for an unnamed person — 탈퇴한 멤버 is said only of a profile whose anonymizedAt the data sets; a profile that is missing from what a page loaded, or whose name is blank, reads 신규 멤버, the word the app always gave a blank name, so an absent profile is never called withdrawn.

  • Lead, 2026-11-15 — the lead's own review, built (batch 63) (record: docs/history/rebuild-log.md "Flat v2 build, batch 63"). The lead's calls, each open to the owner's veto: (1) search hides a blocked pair from each other (it is the door to a friend request and a chat), but the leaderboard does not: a block is silent, so a blocked pair read the same ranks as a stranger; (2) feed rows and cards keep the boards' compact date (MM.DD (요일)), a page's own date line writes it in full (M월 D일 (요일)); (3) the club's sent offers are a group of their own, 보낸 가입 제안, under 가입 신청, which counts only the people who asked to join; (4) the tabs sit in the reading column at desktop width and the day rows are one column; (5) the role $surfaceSecondary is retired: a fill that must be seen takes $gray100, one that sits flush $background. Open as a question for the owner: the month grid on 기록 starts on Sunday and the week strip on Monday; which convention should both follow (L6)?

  • Lead, 2026-11-15 — a required consent is withdrawn only by leaving the service (batch 63, ruling L22), open to the owner's veto and marked for the legal check before launch. Options: (A) the server refuses revoke_consent for a purpose the catalog marks required, (B) accept it and start account deletion from it, (C) hide the button only. Adopted A (migration 00774): one authority, the catalog; a direct call can no longer leave an active account without a required consent; B is a destructive act behind a toggle-shaped call; C leaves the hole.

  • Lead, 2026-11-15 — OPEN (owner question, batch 63 fix round): 초대 or 초대하기 on a band. common.invite is 초대 on the 멤버 관리 FAB, the session band, the guest row and the add-members button. Words 1.19 rules the verb with 하기 on a band and refuses the noun 초대; the terminology guide keeps bare 초대 for a single-word action, and the reviewed boards (list-a, session-hero-quiet, session-detail) draw 초대. Kept as 초대 with a reasoned entry on the words test's allow-list (scripts/__tests__/i18n-words-table.test.mjs). Recommendation: the band and button uses read 초대하기 and the FAB keeps 초대; it needs a second key and the band frames redrawn. Lead, 2026-11-15 — R1, a count on a header icon grows outward (batch 64). Seen: a two-digit mark covered the icon under it. Options: grow inward (as it was), cap at 9+ (loses information), grow outward. Ruling: a one-digit mark stays where the board draws it; a wider mark ("22", "99+") keeps its left edge and grows right (badgePlacement, Badged and the kit's FvkHeaderBadge). No outside rule is cited. Undo: restore the centred placement in badgePlacement. Lead, 2026-11-15 — R2, nothing offers push in the browser (batch 64). Seen: 경기 알림 받으시겠어요? opened on the web, which has no push. Ruling: the web service answers 'unavailable'; the hook returns null until the platform answers; only 'undetermined' asks (once, after the first won seat). Undo: return 'undetermined' from the web service. Lead, 2026-11-15 — R3, the keyboard ring is for the keyboard (batch 64). Seen: a clicked tab kept the ink box. Ruling: the ring is focusVisibleStyle (:focus-visible) at every site; a text field keeps its ring when clicked. Held by the scan in pressable-visual.test.ts (roots: ui, features, app presentation, apps; multi-line form; canary). Undo: pass the ring as focusStyle again (the scan refuses it). Lead, 2026-11-15 — R4, a chat list is ordered by the time it shows (batch 64). Seen: a renamed chat jumped to the top (the list ordered by updated_at, which any write moves). Options: (a) a stored generated column, (b) stop bumping updated_at on writes (rejected: updated_at would lie). Ruling: (a), migration 00775 dm_threads.activity_at = coalesce(last_message_at, created_at); the adapter orders and pages on it. Undo: a migration dropping the column and restoring the two updated indexes. Lead, 2026-11-15 — R5, within a day, upcoming sessions read earliest first and past ones latest first (batch 64). Seen: a pinned in-play session looked out of order in 같은 코트 because the row drew no live state. Ruling: the order code was right (date, live first, start; pinned); the row now carries the canonical SessionStatusBadge. Undo: remove the trailing badge in session-row.tsx. Lead, 2026-11-15 — R6, one mark per corner of a photo (batch 64). Seen: on 클럽 홈 the counter and the 사진 door shared the bottom-right corner. Ruling by the lead: the door keeps the corner. BUILT (the board FvkMediaBand draws a counter and no door; bandCornerMark): the counter with two or more photos, the door 사진 with exactly one. Consequence, a veto point: with two or more photos the hero has no control to the album (a tap on the photo does not open it); the album is reached from the journey feed's photos entry (journey-copy.ts) and from the album list in 클럽 관리. Undo: return the door to the corner and move the counter (the board then changes too). Lead, 2026-11-15 — R7, a form is a draft only after the person changes an answer (batch 64). Seen: opening 일정 만들기 or 클럽 만들기 saved a draft nobody wrote, so "쓰던 내용이 있어요" appeared on the next visit. Ruling: defaults go through the silent setter (the club's region seed, the host's last-used rules); the draft is saved at the person's first edit, or at the door to the rules page (going there is their act: openRulesPage saves the draft first, because that page reads the store). Only these two wizards use the live-draft model. Undo: use the persisting setter in use-region-seed.ts and use-rules-sync.ts. Lead, 2026-11-15 — R8, a pending 제안 상세 has one main action (batch 64). Seen: two equal grey buttons. Ruling: 정보 보완하기 is the band's main button and 제안 취소하기 its secondary; the board frame dtlRcBand follows. Undo: receipt-band.ts and the frame. Lead, 2026-11-15 — R9, Korean sentences say 참석, not "RSVP" (batch 64). Seen: five Korean strings with the Latin word. Ruling: replaced per the terminology guide; the house-style gate refuses \bRSVP\b in the Korean table (with a canary). Undo: remove the rule and restore the strings. Lead, 2026-11-15 — R10, the 정보 list on 일정 상세 folds by content (batch 64, ruled, not built). The facts needed before joining (마감, 취소 규정, 날씨) stay above the fold when the session has them; the four details behind "N개 더 보기" are always the same four. My earlier note of "a fold after five rows" had no source and is withdrawn. Lead, 2026-11-15 — R11, two not-found sentences stay (batch 64, ruled, not built). "삭제됐거나 볼 수 없는 …" where a thing may exist and be closed to the viewer; "삭제됐거나 주소가 바뀌었어요" for things with a public address. Neither tells a viewer that something exists behind the wall. Lead, 2026-11-15 — R12, no-permission walls say what helps on that page (batch 64, ruled, not built). Who may see it, who may change it, or where the person's own version is; the wording is not forced into one pattern. Lead, 2026-11-15 — R13, an offer to join shows in both lists of 멤버 관리 (batch 64, ruled). 보낸 가입 제안 and the person's row in 가입 후보, as docs/canon/clubs.md allows. Built: the tag moves to the row's second line in both rows so the name is not squeezed. Lead, 2026-11-15 — R14, 멤버 관리 keeps two invite actions (batch 64, ruled, not built). The floating 초대 (a member) and the 게스트 초대 card in 게스트 명단 (a guest); the section says which one the card is. Lead, 2026-11-15 — R15, 총 경기 and 승률 mean every completed match with a score, friendlies included (batch 64, decided, NOT built: before launch). Seen: 40 on a person's own 기록 against 78 on their public profile for the same person in the QA world (the own pages read the rated counters, ratedMatchesPlayed; the public profile reads private.player_matches). Options: everything on both, rated on both, two labels. Ruling: one label, one count: everything with a score, which the public profile already shows; the figures that belong to the rating (the ELO block, the rated stage and its minimum) stay on the rated counters. The own pages (기록, 선수별 전적, the host card) need their own read path; scheduled before launch, no code changed now. This batch's fix (mixed doubles counted in the rated total) stands. Lead, 2026-11-15 — OPEN (owner question O1, batch 64): dark mode, soft grey and soft green fills are faint against the page (the pale main button on an unfinished form, letter avatars, grey notices, the dialog panel: 1.26:1 and 1.45:1 against the dark page; text on them is fine at 6:1 or more). Options: lighten the dark greys app-wide (precedent: the neutral Tag, 2026-09-28; every grey surface moves), add a hairline edge to these pieces, or leave it. A palette question that needs eyes on a phone. Not decided. Lead, 2026-11-15 — OPEN (owner question O2, batch 64): achievement tiers are told by colour (bronze, silver, gold) and earned silver looks like a locked grey tile. Options: print the tier word, give silver a distinct non-grey, or one colour for every earned tile. The component library owns the medals; the board prints no tier word. Not decided. Lead, 2026-11-15 — OPEN (owner question O3, batch 64): desktop web only: the side rail's button always says 매치 만들기 while the floating button says 일정 for an official (two different actions, both right). Options: the rail follows the floating button, or stays. Not part of the phone build. Not decided. Lead, 2026-11-15 — S1, dark mode: a soft fill is seen against whatever it sits on (batch 66, ruled on the owner's mark EYES-1; REPLACES the OPEN question O1). The owner: "Yes, the soft grey and soft green do seem too faint against the page. Can't really tell what they are, really. Things that need to be emphasized should be emphasized. Do some research on this end." Options: lighten every dark grey (page, card, fill move together: no gain, whole app), a hairline on four pieces (34 other $gray100 pieces stay faint, borders in a borderless language), leave it (the owner cannot tell what they are), or raise the dark values of the soft-fill roles and add an edge only where a fill cannot differ (chosen: shape readable at 1.5:1 on page AND card, emphasis order page < card < soft fill < solid, no component code but two panels). Built: dark $gray100 = $surfaceTertiary = $tagNeutralFill #363E38, $brandWeak #3A4220, $successWeak #234340, $infoWeak #40365F, NOTICE_DARK_MIX 0.16 to 0.24, a 1px $hairlineEdge on Dialog and ModalPanel's frame, a result-scale base role and a chat-bubble role (found by tests), tertiary text off soft fills; the contrast gate holds the rule with a canary. Light is unchanged. Limits recorded in docs/canon/styling.md: the page and card are not moved, and a Notice tint on a card cannot reach 1.5 without its title falling below 4.5. Lead, 2026-11-15 — S2, achievements: more ornament for a higher tier; locked is an outline (batch 66, ruled on the owner's mark EYES-2; REPLACES the OPEN question O2). The owner: "I think the higher tiers should be more decorated (more embellished = higher tier)". Options: print the tier word (cramped, silver still reads locked), a new silver colour (colour alone), frame plus star count with locked as an outline (chosen: countable in one colour, fits 48 to 56 px, one primitive), a laurel (top tier only, too fine). Built: bronze plain; silver a frame and one star; gold the frame and two; platinum a double frame and three; a locked or hidden tile has no fill and a 1.5px $border outline. No tier word is printed; the spoken label is unchanged. Lead, 2026-11-15 — S3, 총 경기 and 승률: another person's count 경쟁 only, one's own count every match (batch 66, ruled on the owner's mark CALL-5; the owner's ruling REPLACES R15). The owner: "I think the public counts should only show rated matches and personal counts should show all matches. And the detail section should separate the two." Options: everything on both (R15), rated on both, the owner's split (chosen; chess.com and UTR default to rated). Built: migration 00776 (get_player_match_history forces rated-only when the viewer is not the subject, one population for record, rows and the flag; the old four-argument call still resolves), 00778 (the leaderboard's W-L-D is rated only); the public tiles read 경쟁 경기 and 경쟁 승률; one's own 기록 reads one exact server record; one's own 기간 기록 has a 경쟁 and a 친선 row. "Rated" is private.match_rating_is_applied, never a second derivation; a pgTAP case holds the rated count equal to the rating counters. Lead, 2026-11-15 — S4, offers between a blocked pair: the reason is stated to the one who blocked, the other side gets a block-free reason (batch 66, ruled on the owner's mark MEM-6; AMENDS the MEM-6 entry of 2026-11-14). The owner: "We can still make it not possible to send an offer but state the reason." Options: name the block to both (tells an official that a member blocked them; against every opened help page and NNEDV's warning), name it only to the one who blocked with a true block-free reason to the other (chosen), neutral to both (a fault-like line). Built: a row of a person the official blocked says so and has no offer button (the Tag 차단한 사용자 on 가입 후보); get_recommendable_guests leaves out a person who blocked the caller (00777); the offer_unavailable refusal is a statement with its own title and a reason class that never says 차단. NOT built, on purpose: telling an official that a particular member blocked them, because the server cannot tell the sides apart and the disclosure can escalate danger; say so if you want it and it will be built. Lead, 2026-11-15 — S5, the web QA build compiles as the phone does; the public web app does not (batch 67; the owner's to veto). The second W5 build failed on a hook the React Compiler skipped, a compiler the phone build uses and the gate, the web build, its pictures and its journeys did not. Options:

    OptionWhat it does todayRisk and cost
    Never compile the webFinds nothing before a phoneNo risk; nothing to do
    A switch the QA build sets (chosen)Finds compiler-only faults in every QA run; TWOMORE_WEB_REACT_COMPILER=1 in apps/web/next.config.ts, off by defaultNo change to the public web app; the QA build then differs from the public one by the compiler alone
    Always on for the webFinds them everywhereChanges the public web app's behaviour and build with no measurement yet; reversible by one line

    Not measured: the compiled web app's speed or bundle size. The switch stays OFF for the public web app. This is the owner's to veto.

    Owner, 2026-11-15 — S6, preview builds enter through the developer account while the app is being examined (REPLACES "testers sign in by typing" for preview builds). The owner's words: "we always enter through a pre-signed dev account so that we can fully explore all the functionalities without authorization issue. Once we have configured everything correctly from a dev standpoint of view, then we can perhaps look at how different accounts with different access behave in different scenarios." What was done: the two developer names are on the build service's PREVIEW environment again (from the local env file, never printed; the pair was checked to sign in on the hosted project), so the sign-in page of a preview build draws its one-tap "Dev" button (readDevCredentials, no code change). The lead taps that button on the emulator and so can examine signed-in pages on the real build. The lead's limits on it: the pair is readable inside the installed file, so a preview build goes to the owner and the lead only; the production environment holds neither name; the pair comes off before a build goes to other testers. Role and permission testing with other accounts is deferred by the owner until the app's functions are examined; the 1,029 permission probes and the wrong-person journeys already run stay in the gate and the QA runs.

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