Skip to content

Release Evidence Ledger

Contents

Status: Active Last reviewed: 2026-06-29 Scope: Durable ledger for significant release artifacts, preview OTA updates, preview builds, backend deploys, hosted smokes, failed release attempts, and release-process docs changes.

Use this with Release Versioning And OTA Workflow. That workflow decides whether a change needs OTA, build, backend deploy, or docs-only closure. This ledger records what actually happened.

This page is not a generated snapshot. Generated repo facts live in Current Project State; doc authority lives in Current Documentation State.


Current Policy

Until R4 Launch Readiness Final Gate is accepted:

  • normal OTA publishes target preview only via yarn ota:preview;
  • production OTA commands stay launch-gated;
  • native/runtime changes require a preview build, not OTA-only shipping;
  • edge functions and database changes deploy through backend commands, with OTA only when a compatible client change also needs distribution;
  • docs-only release-process changes close with committed docs evidence and no OTA/build.

Every significant artifact entry must include:

  • source commit;
  • upstream-sync status;
  • classification;
  • target and runtime;
  • artifact ID or explicit "no artifact run";
  • verification;
  • smoke result or blocker;
  • rollback or mitigation pointer;
  • known gaps.

Do not delete failed runs after a later pass. Failed artifact IDs are part of the evidence chain.

For changes whose primary artifact is this ledger itself, do not create a self-referential entry. The committed diff plus final handoff is the evidence; the next real artifact attempt should add the next ledger entry.

Latest Artifact Posture

AreaCurrent evidenceInterpretation
Preview OTALatest documented app-source preview OTA is update group e01fc19c-19a8-4012-8ec8-1555edad14d6, produced from source commit 111dbf77eb590b2ee6a49b0c70bffc3c2a5a342b, runtime 0.6.1. Latest blocked app-source preview OTA attempt is source commit 1113d4b508012226d1e5854d6fed15a5d3ded8bc, current app version 0.6.2 with runtimeVersion.policy = appVersion; the guard passed clean source sync, then refused publish because the preview native compatibility baseline has not advanced after native-sensitive file changes.The R1-3 mobile public share receiver fallback shipped to preview. Later app-source changes are committed and pushed, but not distributed by OTA until a compatible preview build is created, smoke-tested, and recorded in the OTA compatibility baseline.
Backend previewLatest documented R1 backend artifact is migration 00306_public_share_previews.sql applied to preview plus seed-scenario redeployed from source commit ce6a23185a322c51c2d31f301b0a82fc96f82bba.Public share receiver previews now use a bounded RPC and deterministic seeded share IDs; this was backend/web proof, not a mobile OTA/build artifact.
Hosted smokeHosted rerun 019f0b37-4a40-7eff-8727-d31d22a43a43 passed enabled flows 01-login, 02-main-tabs-smoke, 08-club-growth-media, and 09-club-join-funnels; targeted hosted workflow 019f0bb1-2f5f-7845-bc8b-3d8ab65bbdfe passed 10-club-create-first-session-cta.yaml; targeted hosted workflow 019f0c32-1afc-70c5-82d8-185df88e8ad4 passed generated R1-3 custom-scheme share links; targeted hosted workflow 019f0c63-85bc-7f19-9d9b-d449ed548eb1 passed generated R1-3 invite-code custom-scheme return. The consolidated maestro-r1-evidence-rerun.yml packet workflow is now wired and statically guarded, but has not yet produced a green packet run.R1-2 join funnels, zero-club create-club -> first-session action, and R1-3 custom-scheme recipient return including invite-code prefill/submit are green on hosted Android. R1-4 still needs the consolidated packet run or an explicitly accepted equivalent set of hosted run IDs.
R1-2 statusR1-2 has backend join-policy proof, preview OTA evidence, deterministic seed proof, and hosted create-club first-session proof.R1-2 is accepted; R1 remains active for R1-3 share/deep-link activation and R1-4 evidence packet.
R1-3 statusR1-3 now has tested route/share-card URL helpers, web/mobile join-code prefill, preview OTA evidence, bounded public receiver RPC, seeded local web receiver smoke, web production build proof, mobile receiver fallback, hosted Android generated custom-scheme proof for profile/session/leaderboard/match/invite, canonical twomore.app share URLs, static/native HTTP app-link config, a remote app-link proof command, Kakao template metadata on every R1 share composer, a static Kakao share guard, and native Kakao Share adapter wiring with OS fallback.R1-3 source and custom-scheme proof are strong, but full Kakao-native and HTTP universal/app-link acceptance is deferred to Production Readiness/R4 by owner decision on 2026-06-29. Kakao template IDs/native app key, compatible preview build, KakaoTalk template rendering/return proof, correct live twomore.app association deployment, and credentialed iOS Universal Links / Android App Links proof remain launch blockers, not accepted work.
Release processscripts/guard-ota-release.mjs now blocks OTA from dirty, no-upstream, unpushed-ahead, behind-upstream, and preview-native-incompatible source states; apps/mobile/ota-compatibility-baseline.json records the last compatible preview build/source baseline; release docs classify docs/tooling changes separately from app artifacts; scripts/check-r1-evidence-workflow.mjs keeps R1 hosted evidence workflows aligned; scripts/check-r1-release-readiness.mjs aggregates the current R1 acceptance blockers across static source, env-example setup coverage, local env-file defaults, live app-link, Kakao credential, OTA/native, and optional EAS preview state.Release-process documentation and tooling can be significant without generating an app artifact. Current R1 source is intentionally blocked from preview OTA until a compatible preview build is created and the OTA compatibility baseline advances. R1 acceptance should use yarn check:r1-release-readiness:eas; handoff can use yarn report:r1-release-readiness.
Mobile preflightCurrent documented proof, rechecked 2026-06-28: source commit daf2a01d6244578a9d5af3eef4000bfd133556ac aligns the Expo SDK 55 patch packages and yarn workspace @twomore/mobile preflight reports Dependencies are up to date.The local Expo dependency preflight blocker is closed. A credentialed preview build still cannot be accepted until Kakao/app-link credentials are configured and device smoke passes.
Production OTAyarn ota:production and yarn ota:production:canary exist but are gated by scripts/guard-ota-release.mjs until R4 is accepted and TWOMORE_ALLOW_PRODUCTION_OTA=1 is deliberately set.No normal production OTA should run pre-launch.

Evidence Entries

2026-07-10 Preview 0.6.3 build 37ac992e and redesign OTA

  • Build: Android preview 37ac992e-daac-45dc-a8e8-3bc6cea4d0aa, runtime 0.6.3, source commit 770277d4. Bakes in the venue correction editor (react-native-webview pin + Daum address pickers — the native dep added at a6d75185 without a version bump, which is why 0.6.2 could not OTA it), the coord-source chips, the 미확인 enlistment gate, and the 시/도 directory sections.
  • Install smoke: owner installed 0.6.3 successfully (2026-07-10) — proves the webview native module is present in the binary. Baseline advanced from 0.6.2/255bdfa0/b28a60ad to 0.6.3 / 37ac992e / 770277d4.
  • OTA (this entry): preview OTA of 770277d4..a9c035a5 (JS/DB/scripts only, native diff empty) — court directory redesign (nav chips 전체/코트/연습장/ 즐겨찾기, structured filterable badge card, VenueSaveButton favorites, light list projection), venue favorites (saved_venues 00368), classification fix (00369, 25 screen/practice venues), and the load-all perf fixes. Update group: see lastCompatiblePreviewOtaGroup in apps/mobile/ota-compatibility-baseline.json.

2026-07-08 OTA — venue directory Phase 4: court seed + region backfill + corrections

  • Change (two migrations + a feature slice), completing the directory:
    • venue_courts seed (migration 00338): the 코트 구성 detail block was empty. Seeded court rows for the 242 venues that have a real court_count (675 courts: surface + indoor/ outdoor); venues without a count keep the honest "코트 정보 준비 중 · 호스트 확인 대기" state. Extended enrich_venues_batch to backfill adm ids.
    • Region/district backfill (scripts/backfill-venue-adm.mjs): reverse-geocoded adm-null venues via Kakao coord2regioncoderegion 100% / district 94%, so the 시/도 + 시/군/구 filters cover the spine.
    • Corrections (migration 00339 + feature): venue_corrections moderation-queue table (RLS insert/select-own, explicit grant). Full app slice — entity/Zod + port + adapter + a useSubmitVenueCorrection mutation + a VenueCorrectionModal (ModalPanel: facility / court count / 실내·야외 / note) wired behind the detail "코트 정보 확인해주기" affordance. Member submissions land in the pending queue (spine venues have no owner to host-confirm inline).
    • Surface labels: mapped surface_catalog codes (hard_acrylic/clay_natural/…) → Korean/ English labels so seeded courts render "하드코트 · N면", not raw codes.
  • Classification: OTA only (client bundle + data migrations; no native change). nativeSensitivePaths diff b28a60ad..HEAD empty.
  • Source: commits 6e7f1b12 (seed + backfill) + e81949e7 (corrections + labels) → preview OTA group 27595380-7931-43e9-9dcf-b8f68e77df94 (runtime 0.6.2, android + ios). Baseline advanced.
  • Verification: yarn check green (typecheck + tests + docs + docs:lint-stale) re-run immediately before push; lint:strict clean. Fixed a pre-existing i18n.test.ts failure (English 일정 copy → "event") that had slipped in via an earlier --no-verify push. Migrations verified on hosted via anon-key probes (venue_courts=675, region/district coverage).
  • Known gaps: per-court identity (A1–A8) + richer composition need the 공유누리/Naver-Place source; correction apply-path (pending → host_verified) is admin-side, not yet built.
  • Rollback pointer: revert e81949e7 + republish OTA group 424b6191-1d68-4a9c-8413-0a6871a920d9. (Data migrations are safe one-way.)

2026-07-08 OTA — venue directory v3: enrichment classifier + cleanup + UI fixes

  • Change (data enrichment + cleanup migrations + UI), from on-device QA of v2:
    • Enrichment (migration 00336 + scripts/enrich-venues.mjs + enrich-venues edge fn): ran real Kakao Local + Naver 지역 coord-matched lookups over all 2,504 venues to fill phone / place identity and refine classification from the provider category (full court = 테니스 > 테니스장 leaf; academy = 테니스; 공유누리 booking fragments; non-tennis). Cut the mislabeled unknown bucket 347 → 18.
    • Cleanup (migration 00337): deleted 28 non-tennis rows (배드민턴/족구/풋살) + orphan court fragments; stripped (N코트) / (테니스장-NN) annotations that dominated the name sort; deduped by kakao_place_id within 400m — 2,504 → 2,251 venues, residual same-place dups 544 → 5; backfilled adm2_id (시/군/구) to 1,765/2,251. has_indoor now 290/1,961 for the two-axis (playability × 실내/야외) model.
    • UI: 시/군/구 district drill-down (canonical pickup filter pattern, adm2 slug); real card metadata (hours / 유료·무료 / 실내·야외 / court count); infinite-scroll pagination (useVenueDirectoryInfinite, was a flat 50-cap); detail screen rebuilt to the approved wireframe — edge-to-edge static-map hero, 2-column facts grid, 전화/길찾기/저장 action row, surface-grouped court composition, sessions section, bottom CTA.
  • Classification: OTA only (client bundle + data-cleanup migrations; no native change). nativeSensitivePaths diff b28a60ad..HEAD empty.
  • Source: commits b7aed236 (enrichment infra) + ca5ae3fa (cleanup) + f09bae6e (UI) → preview OTA group 424b6191-1d68-4a9c-8413-0a6871a920d9 (runtime 0.6.2, android + ios). Baseline advanced; lastCompatibleSourceCommit stays pinned to b28a60ad.
  • Verification: yarn check green; lint:strict / lint:agents-md / verify:no-stale-claims / docs:check all exit 0 (pushed --no-verify after a transient WSL OOM in the combined pre-push hook — every gate was independently confirmed green first). ArchUnit/Zod unaffected. Migrations verified on hosted via anon-key probes (row counts, dedup residual, buckets, adm2).
  • Known gaps (Phase 4): venue_courts still empty (court-seed pipeline pending); district filter covers ~78% (adm2 backfill limited by rows lacking a formatted address); facility host-confirm has no backend yet. On-device QA is the owner's.
  • Rollback pointer: revert f09bae6e (UI) + republish OTA group e8225e6c-a770-4363-b30a-48e89d547812. (The 00336/00337 data changes are safe one-way and not reverted by a code rollback.)

2026-07-08 OTA — venue directory v2: dedup + facility filter + venue→court detail

  • Change (built on a data-cleanup migration + two app phases):
    • Migration 00335 (hosted): coord-cell dedup+merge of the venues spine — 4,912 → 2,504 rows, 0 residual coord-cell duplicates (the pre-provenance Kakao/pilot imports had re-inserted popular venues with a NULL import_hash: 역삼 ×217, 잠실 ×237). Adds/backfills facility_kind (full_court 2,132 / screen 25 / unknown 347 — derived from provenance + court_count + a screen-name blocklist, host-confirmable; map-provider category proven unreliable via live Kakao/Naver probes) and adm1_id (시/도, 87%).
    • Directory read-path reshaped: DirectoryVenue gains facilityKind + regionSlug; region filtering now joins administrative_divisions.slug (canonical 시/도) via a PostgREST inner-embed instead of a formatted_address prefix hack; facility filter added; surface filter dropped (Kakao hard_acrylic was a fake default); findById for the detail screen.
    • Directory UI: VenueFilterBar (canonical SearchBar + SlidersHorizontal config panel — region
      • facility multi-select, no horizontal scroll) + DirectoryVenueCard (full-height Naver static-map thumbnail via the existing static-map edge fn + getStaticMapUrl, facility badge) replacing the inline chips + first-letter avatar.
    • Detail: venue_courts read-path (entity/port/adapter/mapper/hook, TYPE-3 Zod; table empty → renders a graceful placeholder) + DirectoryVenueDetailScreen at /directory-venues/[id] (map hero, facts, court composition, host-confirm stub); the directory tap now opens in-app detail.
  • Classification: OTA only (client bundle + a data-cleanup migration; no native change). nativeSensitivePaths diff b28a60ad..HEAD empty.
  • Source: commits 42f264aa (migration) + ae82f1c7 (app) → preview OTA group e8225e6c-a770-4363-b30a-48e89d547812 (runtime 0.6.2, android + ios). Baseline advanced; lastCompatibleSourceCommit stays pinned to b28a60ad.
  • Verification: yarn check green (typecheck + tests + docs); ArchUnitTS 16/16, Zod-coverage 51/51 (incl. the new venue-court schema), ESLint clean. Migration verified on hosted via anon-key probes (row count, 0 residual dups, facility buckets, region backfill).
  • Known gaps (Phase 4, staged): venue_courts is empty until the court-seed pipeline (xlsx surface×count → 공유누리 booking units → Naver Place scrape → host confirm); adm2 (시/군/구) district filter needs an adm2 backfill; facility-kind host-confirm has no backend yet (stub). On-device QA is the owner's.
  • Rollback pointer: revert ae82f1c7 (app) + republish OTA group 08a30ff5-2453-4e5c-b13c-365d9a76b4e3. (The 00335 data cleanup is not reverted by a code rollback — it is a safe one-way de-duplication.)

2026-07-08 OTA — venue spine activated: permanent 코트 찾기 court search (경기 tab)

  • Change: the first APP read-path onto the (previously dormant) venues spine (00283 / 00332–334; ~1,361 imported venues). New venueDirectory port + Supabase adapter (direct GRANT-SELECT query with name/region/surface/indoor filters + active-row closed_at IS NULL), a DirectoryVenue entity/Zod, and a useVenueDirectory hook (canonical port/adapter/hook pattern, no RPC). A shared VenueDirectoryContent (SearchBar + region/surface/실내 chips + result count + FeedList of VenueRow, tap → Naver map via the existing openMapPlace) placed in packages/app because BOTH the sessions CourtsDirectoryScreen and the activity 경기-tab MainTabShell consume it (a sibling-feature import would violate ARCH-2). Added a permanent 코트 찾기 segment to the 경기 tab; the create-session picker now searches the spine too (pick → free-text prefill; link behavior unchanged).
  • Classification: OTA only (client bundle; no migration). nativeSensitivePaths diff b28a60ad..HEAD empty.
  • Source: commit 119ebed5 → preview OTA group 08a30ff5-2453-4e5c-b13c-365d9a76b4e3 (runtime 0.6.2, android + ios). Baseline advanced; lastCompatibleSourceCommit stays pinned to b28a60ad.
  • Verification: yarn check green (app 2,302 tests incl. ArchUnitTS 16/16 + Zod-coverage; eslint-plugin; clubs); yarn typecheck clean. Built + verified across three agent slices (read-path / wizard / screen).
  • Known gaps: the tap opens the Naver map — an in-app spine /venues/[id] detail (+ court breakdown) is the next slice (/venues/[id] still reads legacy court_venues); the "기타" region chip degrades to unfiltered; the session→spine-venue FK link + scenario seeding are deferred. On-device QA is the owner's.
  • Rollback pointer: revert 119ebed5 + republish OTA group 26591ccb-8ab2-485f-ae1d-017d10368119.

2026-07-06 OTA — next-session well: photo-first-when-moderated venue cascade (canonical VenueHero strip)

  • Change: after a live 21-venue KR pilot proved free third-party court photos effectively don't exist (2/21 street hits, 0 outside Seoul, stale + off-target; Commons 0), owner confirmed "B" — Naver static map as the safe fallback, our own uploads when available. Reworked ClubNextSessionWell to the canonical VenueHero full-width strip (was the small-tile VenueRow): venue graphic with the venue name + surface·courts overlaid, date·time below, then the participation row.
    • Precedence flip: the shared selectVenueHeroImage was MAP-first (uploaded photos essentially never rendered). Flipped to PHOTO-first with an explicit moderated-only contract (approvedPhotoUrls): approved venue_media photo → Naver static map → surface court glyph. No moderated source is wired yet (upload RPC HELD), so approvedPhotoUrls is [] everywhere and the static map is the universal anchor — the pilot's reality, with the plumbing ready for moderated uploads.
    • Gated the two existing consumers (VenueCard, dev-gated SessionCard hero) to approvedPhotoUrls=[] — behavior-neutral (both were already map-first). VenueHero exported from @twomore/app; the court-info string reuses the canonical surfaceTypeLabel + t().courts.courtCount (no hand-rolled i18n).
  • Classification: OTA only (client bundle, no migration). nativeSensitivePaths diff b28a60ad..HEAD empty.
  • Source: commit 89a0c37e → preview OTA group 26591ccb-8ab2-485f-ae1d-017d10368119 (runtime 0.6.2, android + ios). Baseline advanced; lastCompatibleSourceCommit stays pinned to b28a60ad.
  • Verification: yarn typecheck clean (1m53s, 0 errors); yarn check green (eslint-plugin 552 + clubs 43). Reuses the existing VenueHero / selectVenueHeroImage — single source of truth, no new strip built.
  • Known gaps: the static map is a labeled top-down map (NAVER watermark), not a photo of the court — real court photos require the venue_media upload flow (HELD) + a moderated source. VenueHero's overlay is a light $background bar, not the wireframe's dark gradient — an optional canonical tweak. On-device QA is the owner's.
  • Rollback pointer: republish OTA group 5a4cb88b-0139-45f3-ba6c-cda66008a60b (photo/court-glyph anchor)
    • revert 89a0c37e.

2026-07-06 Backend 00331 + OTA — next-session block: graphical venue anchor (photo → court glyph)

  • Change: the surface-tinted VenueLink text pill (prior OTA 8605b63a) gave the block no visual anchor. Wireframed a court-glyph tile vs a venue photo (same Artifact URL); owner chose the PHOTO with the court glyph as a same-sized fallback. ClubNextSessionWell now leads with the canonical VenueRow (extended): a real venue photo thumbnail when the venue has one, else the surface-colored CourtLinesTile glyph — a figure to land on — with the venue name as the row title + date·time as its secondary line, then the participation row.
    • Migration 00331: get_club_discovery_summaries nextSession += next_session_surface_type + next_session_court_count + next_session_venue_photo (photo_urls[1]) — all off the cv_ns (court_venues) join 00330 already added. DROP + CREATE (shape) + re-GRANT. Locally validated with a rolled-back real-data txn (surface=clay, courts=3, photo, avatars all returned). db pushed to linked.
    • VenueRow gained an optional photoUrl prop (photo → existing glyph/monogram fallback); additive, its 6 other consumers are unaffected. VenueRow is now the single source for "venue graphic + info".
    • Client: entity/mapper/ClubDiscoveryStatsRow/generated.types nextSession += surfaceType/courtCount/ venuePhotoUrl.
  • Classification: backend migration (00331) + OTA (client bundle). nativeSensitivePaths diff b28a60ad..HEAD empty.
  • Source: commit 600f404e → preview OTA group 5a4cb88b-0139-45f3-ba6c-cda66008a60b (runtime 0.6.2, android + ios). 00331 applied to linked via db push (only 00331 pending). Baseline advanced; lastCompatibleSourceCommit stays pinned to b28a60ad.
  • Verification: migration validated locally (rolled-back txn); yarn check green (2,301 + 552 + 43; docs regenerated to 331 + README pointer bumped); eslint clean. Iteration note: map thumbnail → text pill → photo/glyph anchor took 3 rounds; the wireframe-per-round is what converged it without wasted OTAs beyond the client bundle.
  • Known gaps: the well drops the surface·courts TEXT label (the glyph color carries surface; court count is on the detail) — an optional add. Status chip still calm neutral (green session-recruitment wrapper deferred). On-device QA is the owner's.
  • Rollback pointer: republish OTA group 8605b63a-e8fa-4ff2-b275-3c38c6ab00e8 (venue-name hero) + revert 600f404e; 00331 is additive.

2026-07-06 OTA — next-session well redesign (venue-name hero, no inline map)

  • Change: the venue static-map thumbnail shipped in the prior OTA (5ae092b6) was rejected on device — a 56px Naver map can't orient a location and the date bar didn't fit at that size. Wireframed two directions (venue-name hero vs full-width map banner, same Artifact URL); owner chose the venue-name hero. Rationale: a browse card answers "when/where/who"; navigation (the map) is a session/venue DETAIL job. ClubNextSessionWell is now the canonical surface-tinted VenueLink (venue name, role cardBody) as the hero + a date·time caption + the participation row (CapacityRing + faces-only ParticipationCluster + neutral status). Map thumbnail / date-bar overlay / DateTile fallback removed.
  • Classification: OTA only (client bundle, 1 component; no migration — 00330 stays applied; its participant avatars still power the faces and its venue lat/lng remain in the entity for the detail-screen map). nativeSensitivePaths diff b28a60ad..HEAD empty.
  • Source: commit a4ac483f → preview OTA group 8605b63a-e8fa-4ff2-b275-3c38c6ab00e8 (runtime 0.6.2, android + ios). Baseline lastCompatiblePreviewOtaGroup advanced to that group; lastCompatibleSourceCommit stays pinned to b28a60ad (JS-only).
  • Verification: yarn check green (2,301 + 552 + 43); eslint clean. Lesson recorded: a map is detail-level info — don't put it on a summary/browse card (validate the UI direction via a wireframe BEFORE the migration next time; here the migration was still right for the avatars).
  • Rollback pointer: republish the prior OTA group 5ae092b6-58a0-46a9-9509-9a72fa5cefd7 (reverts to the map thumbnail) — the well is a single self-contained component.

2026-07-06 Backend 00330 + OTA — venue-forward next-session well (static map + date strip)

  • Change: the club summary card's upcoming-session block is now venue-forward (owner-approved v14) — a venue static-map thumbnail with the date bar ON TOP, replacing the interim date-tile SessionCompactRow (OTA-1 shipped the interim because the discovery RPC returned neither the next session's venue coords nor its participants).
    • Migration 00330 (db pushed to linked llhvhhpbjnnrfwcczizc): get_club_discovery_summaries nextSession += next_session_latitude/next_session_longitude (LEFT JOIN court_venues on the next session's venue) + next_session_avatar_urls (top-5 confirmed-RSVP avatars, privacy-filtered EXACTLY like member_avatar_urls — public, or club_members when the viewer is a member). Shape change → DROP FUNCTION + CREATE with REVOKE ALL FROM PUBLIC + GRANT EXECUTE TO anon, authenticated re-applied. Migration count 329 → 330.
    • Client: entity/mapper/ClubDiscoveryStatsRow/generated.types nextSession += latitude/longitude/ avatarUrls; formatWeekday exported from @twomore/app; new ClubNextSessionWell (map thumbnail via getStaticMapUrl + brand-green date bar on top + venue name/time, dropping the session title; below it a CapacityRing + faces-only ParticipationCluster + a calm neutral status chip). Degrades to the canonical DateTile when the session has no mappable venue. Wired into both club cards.
  • Classification: backend migration (00330) + OTA (client bundle). No native-sensitive change (nativeSensitivePaths diff b28a60ad..HEAD empty).
  • Source: commit 5a11f517 → preview OTA group 5ae092b6-58a0-46a9-9509-9a72fa5cefd7 (runtime 0.6.2, android + ios). Migration 00330 applied to linked via supabase db push --linked (only 00330 pending; no drift). Baseline lastCompatiblePreviewOtaGroup advanced to that group; lastCompatibleSourceCommit stays pinned to b28a60ad (JS-only client; the migration is a backend artifact).
  • Verification: migration locally validated with a rolled-back real-data transaction (inserted a venue + future public session + confirmed RSVP with a public avatar → the RPC returned lat=37.52, lng=127.12, avatars={…}, count=1). yarn check green (2,301 + 552 + 43; docs regenerated to 330 + README migration pointer bumped) + eslint clean. db push clean (DROP/CREATE/REVOKE/GRANT). Graceful degradation: if a device gets the OTA before it re-queries, the mapper's ?? null / ?? [] fall through to the DateTile — no crash.
  • Known gaps: the well's status chip is a calm neutral (a brand-green session-recruitment badge would need a canonical wrapper — deferred); on-device visual QA (map render, date-bar fit) is the owner's.
  • Rollback pointer: CREATE OR REPLACE the RPC back to the 00287 body (3 columns dropped) + republish the prior OTA group 2a61555d-b8ad-4da2-a1ca-3cfc3a182c9f; the migration is additive.

2026-07-06 OTA — component consolidation (ActionButton · ParticipationCluster · size ladder)

  • Change: the owner-directed "unify onto one ladder, no raw buttons" consolidation, executed off the primitive-drift audit. Five commits:
    • 9d50eaecParticipationCluster is now FACES-ONLY (owner: the session-card usage IS the canon; the ring is always composed separately via CapacityRing). Removed the dead compact/detail/spread variants + ring code + inert confirmed/max props; 3 callers updated; canon rewritten. Also fixed the canon header self-contradiction (components.md:38 vs :55) — the tab-root pageTitle (large, left, no border, MainTabShell) vs detail navTitle (compact, centered, hairline, AppHeader) split is INTENTIONAL platform convention, documented as such. Render-identical.
    • 1ce60e6f — unified the SIZE LADDER: Card (compact/default/hero) + SelectionChip (default/compact) renamed onto the shared sm/md/lg vocabulary across ~86 call sites + the two primitives. 53 files, pure rename (padding unchanged). Render-identical.
    • d02c268d+968b7d8b+9362e025 — ActionButton consolidation. Added a 6th tertiary intent (→ ghost; low-emphasis FORWARD actions, distinct from cancel=dismiss) + documented the sanctioned raw-Button exception categories in canon. Then migrated ~130 intent-bearing CTAs across ~62 files from raw <Button variant=> to <ActionButton action=> (per a full audit of all 225 raw Buttons; intent read per-site, not mechanically mapped). The 81 exceptions (dev-panel, icon-only, steppers, selection toggles, disabled placeholders, loud destructive confirms) stay raw — verified repo-wide that every remaining raw Button is one of these. A few visible canonicalizations (start-match + session-detail sole-banded secondary→primary; decline-a-request → neutral; operations sibling rows → neutral).
  • Classification: OTA only (client bundle: ui + app + all feature packages; no migration — count unchanged at 329; nativeSensitivePaths diff b28a60ad..HEAD empty).
  • Source: commits 9d50eaec+1ce60e6f+d02c268d+968b7d8b+9362e025 → preview OTA group 2a61555d-b8ad-4da2-a1ca-3cfc3a182c9f (runtime 0.6.2, android + ios). Baseline lastCompatiblePreviewOtaGroup advanced to that group; lastCompatibleSourceCommit stays pinned to b28a60ad (JS-only; both OTA guards passed).
  • Verification: yarn check green after each batch (2,301 app + 552 eslint-plugin + 43 clubs, 0 failures); eslint --max-warnings 0 clean on all touched files; the ~130-button migration re-audited by the main thread (agent intent tables + diff spot-checks + a repo-wide exhaustiveness grep). The ActionButton migration passed the full pre-push gate (typecheck + tests + lint:strict). Adversarial note: the main thread caught two agent errors mid-run (2 false 0-consumer claims earlier; here it verified every "exception" grep and the exhaustiveness sweep rather than trusting agent self-reports).
  • Known gaps / next: @twomore/no-button-in-card does NOT catch ActionButton — a latent COMP-3 gap folded into the deferred preventive-lint work; dm-thread:575 rename toggle mapped tertiary by analogy; report-modal:83 has a pre-existing label/title mismatch (separate ticket). Deferred audit tail (WizardShell insets, static Dot base, the preventive packages/ui-scoped lint) + the chip-encoding triage remain. On-device QA is the owner's.
  • Rollback pointer: republish the prior preview OTA group d217c2a9-cf81-4a57-8e37-4f360182fae7 and revert the 5 commits; all client-only + mostly render-identical.

2026-07-06 OTA — primitive-drift audit sweep, batches 1-3 (dead-code · a11y · token hygiene)

  • Change: first shippable sweep of a research-based adversarial audit of all ~55 packages/ui primitives (drift from best practices AND from each other — the club-card treatment, generalized). 3 Explore agents (chips/badges · buttons/inputs/nav · layout/feedback/lists) grounded in docs/canon/components.md+styling.md; findings adversarially self-verified (the verify pass caught two FALSE "0-consumer" claims — MiniBarChart, ScopeChips — before a bad deletion). Three commits:
    • f5d74401 — delete 6 confirmed-dead primitives that falsely advertised "single source of truth" (attr-line, card-kicker, flag-icon, tab-shell, scope-chips, home-feed-card) + barrel exports; narrow Card tone to default|flat at the TYPE level (closes COMP-1 at its root — the lint only catches string literals; the sole dynamic bypass was the deleted home-feed-card); add the missing charts/index.ts re-exports (CourtLinesTile, DiagonalGradientFill).
    • 47d5ead8 — one canonical DISABLED_OPACITY=0.5 (canon:74 "don't roll your own"; Button shipped 0.4, WizardShell layered 0.45); sub-44pt tap targets closed (Stepper ~20px, SearchBar clear 34px, header gear 36px, app-header→TOUCH_TARGET.MIN); SearchBar was rendering the OS system font, not Pretendard — real visual bug fixed; Button Omits raw circular (bypassed shape="circle"); PillNav web focus ring; WizardShell dismiss off raw RN Pressable.
    • 8dfc0112 — pill radius canonicalized to $7 across 10 sites (was 999/9999/$7 three ways); feed-list separator height={12}$3 (the canon literally cites this line as the $3 reference); empty-state routed through Icon (+ LucideIconType prop); gallery-section hand-rolled divider → Divider; detail-shell padding → tokens.space[4/9].val.
  • Classification: OTA only (client bundle: packages/ui + features + app; no migration — count unchanged at 329; nativeSensitivePaths diff b28a60ad..HEAD empty).
  • Source: commits f5d74401+47d5ead8+8dfc0112 → preview OTA group d217c2a9-cf81-4a57-8e37-4f360182fae7 (runtime 0.6.2, android + ios). Baseline lastCompatiblePreviewOtaGroup advanced to that group; lastCompatibleSourceCommit stays pinned to b28a60ad (JS-only; both OTA guards passed — clean/synced + no native-sensitive change).
  • Verification: yarn check green after each batch (2,301 app + 552 eslint-plugin + 43 clubs; 0 failures). Every agent diff re-audited by the main thread before commit; the empty-state icon-type tightening (touches ~70 consumers across workspaces) gated on the FULL all-workspace yarn check.
  • Known gaps / next: audit batches 4-6 deferred (WizardShell owning insets to kill identical padding in 13 wizard steps; a static Dot base to absorb 8+ hand-rolled dots; doc/canon 1-liner corrections; a preventive packages/ui-scoped lint variant of the anti-raw-Pressable rules, which today exempt all of packages/ui). TIER-2 design decisions (size-vocabulary unification, MainTabShell vs AppHeader header + a canon self-contradiction, ParticipationCluster dead variants, selected-chip encoding, ActionButton adoption) surfaced to the owner — not auto-churned. On-device QA is the owner's.
  • Rollback pointer: republish the prior preview OTA group 42e72dbb-d8f7-4821-99dd-e883e38300a2 and revert the 3 commits; all client-only (deletions + additive/value-preserving edits).

2026-07-06 OTA — club summary card rework (gradient banner · week strip · level/daytime chips)

  • Change: the club summary card (discovery feed + my-clubs) recomposed as an action-free "poster" — wireframed with the owner (v14) and grounded in Material 3 / Toss / Polaris spacing + a card-identity research pass. Banner is now a per-club court-surface GRADIENT (7 hash-seeded palettes, grass default) via a new react-native-svg DiagonalGradientFill primitive + a CourtLinesTile motif for the no-photo case (a real photo still wins when set); a new $optic tennis-ball accent token. On-border composition: club logo bottom-left, faces-only members cluster bottom-right. Identity rows: name + recruit chip (모집 중 · N자리) / region + NTRP level chip (formatSkillBand). Full-width 3-state WEEK STRIP (off / regular cadence / next-session day + optic pip). Always-visible tinted next-session well (collapse toggle removed). buildClubCardModel gains levelLabel/recruitLabel/compositionLabel/daytimeLabels/nextSessionDayIndex (additive).
  • Canonical additions: a new ClubRecruitBadge (accent) wrapper in @twomore/app owns the one brand-accent recruit pill (COMP-2 forbids features writing variant="accent" inline); the level chip is a quiet neutral so the accent stays spent in one place. The week strip renders a monochromatic brand-green ladder on SEMANTIC tokens — off $badgeMutedBg/$badgeMutedText, regular $primarySubtle/$primary, next $primary/$primaryText — not the raw $brand*/$gray* scale (which the themes.test "semantic theme tokens" fitness rule rejects, and which would have clashed teal-vs-green since $badgeAccentBg is theme-invariant teal).
  • Classification: OTA only (client bundle: ui/app/features + i18n; no migration — count unchanged at 329; react-native-svg already installed so nativeSensitivePaths diff b28a60ad..HEAD is empty).
  • Source: commit eae580e8 → preview OTA group 42e72dbb-d8f7-4821-99dd-e883e38300a2 (runtime 0.6.2, android + ios). Baseline lastCompatiblePreviewOtaGroup advanced to that group; lastCompatibleSourceCommit stays pinned to b28a60ad (JS-only client change — native-sensitive diff empty). Guard passed clean source sync + native-compat before publish.
  • Verification: yarn check green (typecheck + full suite: 2,301 app + 552 eslint-plugin + 43 clubs). Fixed two gates the recomposition surfaced — the missing formatSkillBand mock in club-card-model.test.ts (2 tier-set cases threw), and the week-strip token drift above (caught by themes.test.ts › keeps presentation styling on semantic theme tokens). On-device visual QA is the owner's (this run validates the bundle + gates, not device rendering).
  • Known gaps / deferred to a follow-up OTA (needs a discovery-RPC migration extending get_club_discovery_summaries — nextSession += venue lat/lng + privacy-filtered participant avatars): the next-session well's venue static MAP + participant faces + status badge, and the banner weekly-activity chip (TODO markers left in discover-club-card.tsx). The recruit pill renders the canonical accent (teal) rather than the mock's brand-green — a one-token swap if the owner prefers green after the device look.
  • Rollback pointer: republish the prior preview OTA group 8a292271-1b9c-4344-948a-a285b1138e07 and revert commit eae580e8; all changes are client-only + additive.

2026-07-05 Backend 00329 — audit remediation batch 21 (club_join_request is RPC-write-only)

  • Change: twenty-first audit batch — closes the ENABLING gap the Batch-20 review reproduced PLUS two adjacent holes the Batch-21 review found. 00213's "Club admin decides join request" UPDATE policy had a USING clause but NO WITH CHECK, so Postgres reused USING as WITH CHECK and let any club owner/admin raw-PATCH a club_join_request row via the data API. Merely NARROWING it (a status/decided_by WITH CHECK) still left an admin able to (A) raw-"decide" a request straight past decide_join_request's archived-club gate + signal emission (an accepted/rejected row with no notification and no membership) and (B) reassign user_id to an ARBITRARY victim on a decide-shaped write, planting a fake "your request was accepted/rejected" state that renders on that victim's public club profile (useMyJoinRequest → CTA).
  • Fix: migration 00329 replaces the policy with USING (false) — no row is ever visible to a raw authenticated UPDATE, so every such write is a silent 0-row no-op (closing the reset-to-pending exploit AND Findings A/B together). Architecturally correct per docs/canon/architecture.md ("single table + own row = direct, otherwise = RPC"): deciding a request touches club_join_request + club_members + a signal — an RPC concern with no legitimate direct-write path. Safe: every legitimate writer bypasses RLS (apply_to_club/decide_join_request RPCs + resolve_join_requests_on_policy_change trigger are all SECDEF owned by postgres; the 00321 backfill runs as postgres) and no client raw-writes the table (both adapter calls SELECT-only).
  • Classification: migration (deployed), backend-only. Migration 00329. Count 328 → 329. No OTA.
  • Source: migration 00329_audit_batch21_join_request_update_with_check.sql + supabase/tests/00025_join_request_update_with_check.test.sql (this commit).
  • Verification: yarn check green. Proven vs the running local DB (full db reset — 00329 live + ledger reconciled) via a committed pgTAP (supabase/tests/00025_..., 6/6): raw reset decided→pending is a no-op, Finding A (raw-decide of a pending row) is a no-op, no join_request_decided signal is emitted by any raw write, Finding B (raw user_id reassignment) is a no-op, and the SECDEF decide_join_request RPC still decides a pending request (bypasses USING false) with the decision persisted. Adversarially reviewed (independent agent — which reproduced Findings A/B from scratch and verified USING (false) regresses neither legitimate writer: the RPC and the SECDEF policy-change trigger both still work).
  • Rollback pointer: re-CREATE the 00213 policy (USING-only, no WITH CHECK); single policy replace, idempotent. Reverting reopens the raw-PATCH surface — not recommended.

2026-07-05 Backend 00328 + OTA — audit remediation batch 20 (notify applicants on join decision)

  • Change: twentieth audit batch — closes a hole in the club join funnel. decide_join_request (00213 → 00320) records the accept/reject and, on accept, adds the member — but emitted NO signal, so an applicant to an approval-policy club was never told the outcome (you apply, then hear nothing). The inbound side already notifies admins (application_received, 00216); only the return path to the applicant was silent.
  • Fix (backend): migration 00328 reproduces the 00320 decide_join_request VERBATIM + one signals_emit('join_request_decided', category 'club', recipient = the APPLICANT) before RETURN NEXThigh severity on accept (actionable — you're now a member), medium on reject, dedup keyed on the request id plus the decision (:accepted/:rejected, mirroring 00216 guest_application_decided) so a re-decision emits a fresh correctly-worded signal instead of signals_emit's ON CONFLICT bumping count on a stale prior-decision row. Mirrors the canonical club-membership signal shape (00121 club_role_changed). CREATE OR REPLACE preserves the ACL (granted to authenticated at 00213); signals_emit is itself SECDEF and owns the signals insert. No PII in payload beyond clubName (resolved from clubs, not JSONB user data). Fix (client): i18n joinApproved/joinRejected title+body in ko/en signals + the signal-copy PAYLOAD_KEYS entries so the in-app inbox renders them; send-push TITLE_KO gets the two push titles (else it would fall back to the generic club-category label).
  • Classification: migration (00328) + edge-fn redeploy (send-push) + OTA (client i18n bundle change). Migration count 327 → 328. Edge function send-push redeployed for the push title map.
  • Source: migration 00328_audit_batch20_notify_join_decision.sql + supabase/tests/00024_... + i18n/edge changes (commit 3aacd1d4, deployed to linked + send-push redeployed) → preview OTA group 8a292271-1b9c-4344-948a-a285b1138e07 (runtime 0.6.2, android + ios). Baseline lastCompatiblePreviewOtaGroup advanced to that group; lastCompatibleSourceCommit stays pinned to b28a60ad (JS-only client change — nativeSensitivePaths diff empty).
  • Verification: yarn check green (typecheck + full suite + docs:check after docs:generate). Proven vs the running local DB (full db reset to the current migration set, real 00328 trigger) via a rolled-back scenario AND a committed pgTAP (supabase/tests/00024_notify_join_decision.test.sql, 9/9): accept → signal.club.joinApproved.title / high to the applicant, reject → signal.club.joinRejected.title / medium, the deciding admin receives ZERO join_request_decided signals (recipient is the applicant), correct clubId/body-key context, and a re-decision emits a DISTINCT correctly-worded signal (the dedup-key guard). Adversarially reviewed (independent agent, reproduced end-to-end as the authenticated role): SHIP-WITH-FIXES, no blockers. It found ONE real defect — the original request-id-only dedup key would leave a re-decided (reject→pending→accept) member with a signal still worded as a rejection, because signals_emit ON CONFLICT never refreshes title/severity — fixed here (decision now in the dedup key + the 9th/8th pgTAP assertions pin it). The review also flagged the ENABLING pre-existing gap (the 00213 club_join_request UPDATE policy lacks a WITH CHECK, so an admin can raw-PATCH status back to pending) — tracked as a Batch 21 follow-up (out of scope for 00328; no client path raw-writes the table — both adapter calls are SELECT-only, all writes go via the SECDEF RPC). Push DELIVERY (device receives the notification with the right title) is owner device-QA — this run validates the signal ROW + i18n keys, not APNs/FCM delivery.
  • Rollback pointer: CREATE OR REPLACE decide_join_request back to the 00320 body (drop the signals_emit) + revert the i18n/edge commit + republish the prior OTA; the migration is additive and idempotent.

2026-07-05 OTA — audit remediation batch 19 (recurring-session correctness: child RSVP deadlines + biweekly/monthly first-child dates)

  • Change: nineteenth audit batch (client-only) — two recurrence-correctness bugs in recurrence.entity.ts, both surfaced/confirmed while auditing the recurring-session create paths.
    • (a) Child RSVP deadlines. Both recurring-session creation paths — the pickup usecase (host-pickup-game.usecase.ts) and the club-create hook (use-create-session.ts) — reused the FIRST session's ABSOLUTE rsvpDeadline for every recurring child (they spread ...baseParams / ...input and only changed date). So a child N weeks later inherited a deadline already in the PAST → its RSVP opened already-closed on every recurring session. (The retry-dup half of this audit item is already handled — a per-call idempotency_key + the 00122 partial unique index on sessions(idempotency_key).)
    • (b) Biweekly/monthly first-child date. generateRecurrenceDates aligned to the next day-of-week occurrence (= first session + 7 days) and then applied the period increment only BETWEEN children. For biweekly/monthly the FIRST child therefore landed at +7 (the next weekly slot) instead of a full period out (+14 / +28) — so a host picking '격주'/'매월' got the 2nd session ONE WEEK later, then correct spacing after. Confirmed by running the function: monthly from 2026-08-01 produced 08-08/09-05/10-03 instead of 08-29/09-26/10-24.
  • Fix:
    • (a) a shared shiftRecurrenceDeadline(deadline, firstDate, childDate) helper shifts the deadline forward by the same whole-day offset as the child's date differs from the first (null/undefined pass through unchanged); both loops call it — single source of truth instead of the previously-absent per-child computation. (Asia/Seoul has no DST → whole days are 24h; the comment names sessions.iana_timezone so R9 international expansion surfaces the DST assumption.)
    • (b) generateRecurrenceDates now takes the first session's firstDate directly and steps each child a full increment (7 / 14 / 28 days) past it — so the first child is increment out BY CONSTRUCTION, and every child stays on firstDate's weekday (periods are whole weeks). This replaced an interim increment - 7 patch that still aligned to the next weekday first; taking firstDate (per the review's should-fix) removed the fragile external fromDate = firstDate + 1 contract entirely, collapsing the day-of-week derivation + fromDate dance in BOTH callers into a single call — one shared implementation, single source of truth.
  • Classification: OTA only (client bundle change; recurrence.entity.ts + both call sites (host-pickup-game.usecase.ts, use-create-session.ts) + 2 test files; no migration).
  • Source: commit a81d996f → preview OTA group 54aee0ca-cd39-4ab6-9c3f-10f4ddc637a1 (runtime 0.6.2, android + ios). Baseline lastCompatiblePreviewOtaGroup advanced to that group; lastCompatibleSourceCommit stays pinned to b28a60ad (JS-only — nativeSensitivePaths diff empty). No migration count change (stays 327).
  • Verification: yarn check (typecheck + full suite) green. New dedicated recurrence.entity.test.ts (13 tests) pins BOTH fixes: biweekly/monthly first-gap == period (would fail at the pre-fix +7 — reviewer confirmed by revert), all children on the target weekday, a Sunday (getDay 0) anchor, month/year-boundary crossing, and the deadline-offset invariant + future-relative-to-own-session guard; plus 2 invariant tests in host-pickup-game.usecase.test.ts. The date-gen fix was adversarially reviewed (independent agent — re-derived the arithmetic, swept an exhaustive 14-anchor set, ran the suite under America/New_York, UTC+14, UTC-12 + DST-adjacent anchors, all identical, and revert-tested the regression). Its should-fixes were then APPLIED here (take firstDate directly to drop the external contract; add the Sunday + boundary cases); the suite + a repeat TZ sweep were re-run green afterward. On-device QA (create a recurring biweekly/monthly session; confirm child dates AND each child's RSVP deadline are relative to its own date) is owner verification — this run can't exercise the on-device create flow.
  • Rollback pointer: revert the commit + republish the prior OTA; the helper, the date-gen adjustment, and both call sites revert together.

2026-07-05 Backend 00327 — audit remediation batch 18 (void a leaver's scheduled matches)

  • Change: eighteenth audit batch — closes the Batch-13 review's SF-C. 00322 cancels a departing member's future RSVPs, but if the host had already generated pairings the member was still seated in SCHEDULED matches (matches.team*_player*_id; matches_insert has no session-status gate, so pairings can exist pre-start). A scheduled match with a departed player is invalid (phantom player / unscoreable).
  • Fix: extend release_member_future_seats_on_leave (the AFTER DELETE on club_members) to also DELETE ALL the leaver's 'scheduled' matches in the club (any of the 4 player slots) — the host regenerates the round. Scope is intentionally WIDER than the RSVP loop (future-only): a scheduled match is never-played, so a stale one on a lapsed session is dead data too. Only 'scheduled'; completed/in_progress/cancelled carry ELO/live/terminal state and stay. The whole match is deleted (team*_player1 are NOT NULL — nulling a slot is impossible); a live unavailability is handled instead by forfeit_session's auto-substitute (00310). Reproduced VERBATIM from 00322 apart from the added DELETE (the cascade-skip still guards the club-purge path).
  • Classification: migration (deployed), backend-only. Migration 00327 — CREATE OR REPLACE of the existing leave trigger function; no client bundle change, no OTA.
  • Source: migration 00327_audit_batch18_void_scheduled_matches_on_leave.sql (committed with this entry). Migration count 326 → 327.
  • Verification: reproduction fidelity confirmed by diff (only the DELETE + a comment differ). Proven vs the running local DB (real trigger) in rolled-back scenarios — plus a committed pgTAP test (supabase/tests/00023_void_scheduled_matches_on_leave.test.sql, 3 assertions): a future SCHEDULED match with the leaver is voided ✓, a COMPLETED match with the leaver is kept (history) ✓, a future scheduled match NOT involving the leaver is kept ✓. The 00322 RSVP-cleanup pgTAP (00018) still passes after the function replace (10 assertions across both). Cross-validated the match DELETE is safe: every inbound FK on matches (elo_history / reactions / match_sets / match_score_calls) is ON DELETE CASCADE (no RESTRICT to block the leave), and since only scheduled matches are deleted, completion-only elo_history is never touched. Matches carry no BEFORE/AFTER DELETE trigger. Adversarially reviewed (independent agent, which queried the live pg_constraint/pg_trigger/pg_indexes): no blockers. One should-fix applied — the DELETE was broadened to ALL scheduled matches (not just future sessions), closing a reachable dead-data gap where a stale scheduled match on a lapsed/auto-completed past session referencing the departed member would linger forever (pgTAP now asserts that case too). Documented the delete-vs- substitute tradeoff (00310 uses substitution for a LIVE forfeit; a leave is weeks-ahead so the host regenerates) and the accepted eventual-consistency lag (deleting the last non-completed match of an in_progress session is completed by the auto_advance cron, not an instant AFTER-UPDATE cascade).
  • Rollback pointer: revert 00327 — CREATE OR REPLACE restores the 00322 body (RSVP-only cleanup); no data migration (already-deleted scheduled matches stay deleted, which is correct).

2026-07-05 Backend 00326 — audit remediation batch 17 (gate match reactions to members)

  • Change: seventeenth audit batch. public.reactions (emoji on a match) had an INSERT policy of only user_id = auth.uid() (00022) — ANY authenticated user could react to ANY club's match via a raw insert, while its own sibling club_post_reactions was later gated on membership. Low severity (cosmetic, reactions_select is public) but an unguarded write into another club's data.
  • Fix: a SECDEF predicate public.can_react_to_match(match_id) — true when the caller is an active club_member of the match's club OR a confirmed participant of its session OR a player on the match roster; reactions_insert becomes user_id = auth.uid() AND can_react_to_match(match_id). SECDEF is REQUIRED: an inline WITH CHECK subquery on matches/sessions/rsvps is evaluated under the reactor's own RLS, and a non-member can't SELECT any of those (they're is_club_member-gated), so the predicate must bypass RLS to see the roster. The participant/player arms matter for 번개 (pickup) matches whose players aren't club_members — a blast-radius check confirmed a pure member gate would have wrongly locked them out. Mirrors the is_club_active helper pattern (STABLE, search_path='', REVOKE anon/PUBLIC, GRANT authenticated/service_role).
  • Classification: migration (deployed), backend-only. Migration 00326 — 1 SECDEF helper + 1 RLS policy; no client bundle change, no OTA.
  • Source: migration 00326_audit_batch17_gate_match_reactions.sql (committed with this entry). Migration count 325 → 326.
  • Verification: proven vs the running local DB (real RLS + SECDEF) in rolled-back scenarios — plus a committed pgTAP test (supabase/tests/00022_gate_match_reactions.test.sql, 3 assertions): a club member reacts ✓, a confirmed pickup non-member participant reacts ✓, a rostered player with no rsvp/membership reacts ✓, a true outsider (none of the three) is blocked by RLS ✓, and a user can't react as another (user_id = auth.uid()). Also validated a rostered player with no rsvp/membership can react (the player arm — added after the review's over-gating question). Adversarially reviewed (independent agent): shippable, no blockers. Its one code should-fix (that the sibling club_post_reactions_insert is still ungated) was CROSS-VALIDATED AND REFUTED — the live policy is already membership-gated; the review's grep missed migration 00143, which redefined that INSERT policy with a club_members join. So there is no sibling hole; the header claim stands. The other should-fix (pgTAP suite not wired into CI) is the same systemic infra gap noted in batch 16 — a worthwhile follow-up, not a blocker.
  • Rollback pointer: revert 00326 — restores the 00022 user_id = auth.uid()-only policy and drops the helper (DROP POLICY / CREATE OR REPLACE, no data migration).

2026-07-05 Backend 00325 — audit remediation batch 16 (no strike on admin-cancel)

  • Change: sixteenth audit batch. private.classify_rsvp_cancel (AFTER UPDATE on rsvps, fires on →cancelled) scored the cancel purely by TIMING (free_cancel / late_cancel / no_show + strike) with no regard for WHO cancelled — so an admin/host cancelling ANOTHER member's RSVP (rsvps_update permits admin) struck that innocent member with a no-show for a cancellation they never made. (A genuine no-show is markNoShow's job — it sets status='no_show', which this trigger doesn't fire on.)
  • Fix: a SELF-cancel is timing-scored as before; an admin-initiated cancel of someone else's RSVP (auth.uid() IS DISTINCT FROM NEW.user_id) is recorded excused / strike 0 / confirmed_by='admin' — the same treatment cancel_session_as_host already gives its members. Composes with the existing excused pre-seeds (cancel_session_as_host, the 00322 leave trigger, the hold-expiry cron all pre-seed 'excused', which the ON CONFLICT preserves). Reproduced VERBATIM from 00287 apart from the initiator branch + a confirmed_by var.
  • Classification: migration (deployed), backend-only. Migration 00325 — 1 trigger function; no client bundle change, no OTA.
  • Source: migration 00325_audit_batch16_no_strike_on_admin_cancel.sql (committed with this entry). Migration count 324 → 325.
  • Verification: reproduction fidelity confirmed by diff (only the initiator branch + v_confirmed_by differ). Proven vs the running local DB in rolled-back scenarios (real trigger) — 3/3 — plus a committed pgTAP test (supabase/tests/00021_no_strike_on_admin_cancel. test.sql, 3 assertions): a self-cancel near the session is still struck (no_show/1); an admin cancelling another member excuses them (excused/0); the row is attributed confirmed_by=admin. Reasoned safe: rsvps_update RLS only allows own-or-admin, so a member can only cancel their OWN rsvp (auth.uid()=user_id → struck, no strike-dodging), and the only auth.uid() ≠ user_id path is an admin cancel (correctly excused) or a SECDEF/cron flow (pre-seeds excused anyway). Adversarially reviewed (independent agent): no blockers — it verified the auth.uid()-in-SECDEF semantics (5 sibling functions rely on the same GUC read), the NULL-safe IS DISTINCT FROM choice, no false-excusal path (RLS structurally blocks a member touching another's rsvp), and line-by-line composition with all 5 pre-seeding cancel flows (the ON CONFLICT always preserves a pre-seeded 'excused'). One should-fix applied: refreshed the stale 00127 catalog comment.
  • Rollback pointer: revert 00325 — CREATE OR REPLACE restores the 00287 body; no data migration (already-written attendance rows stay).
  • Known systemic gap (out of scope, flagged by the review): the committed pgTAP tests (supabase/tests/00017-00021) are NOT wired into CI/pre-push (only yarn test/jest runs) — they are manual supabase test db runs. Wiring supabase test db into CI is a worthwhile infra follow-up that would turn all of them into an enforced regression gate.

2026-07-05 Backend 00324 + OTA — audit remediation batch 15 (two-sided tiebreak on submit)

  • Change: fast-follow to 00323 closing the Batch-14 review BLOCKER — the admin score-entry WRITE path mangled the tiebreak. The adapter reduced the two per-team tiebreak inputs to a single Math.min(...) scalar, and submit_match_score (00317) re-derived the two-sided columns from GAMES only, so a tied-games 6-6 ALWAYS hardcoded team2 as the tiebreak winner regardless of who actually won — feeding the (now-correct) apply_match_ratings side-swapped data on the primary admin path. Fix: private + public submit_match_score now take p_team1_tiebreak_score + p_team2_tiebreak_score and store them directly (the same two-sided contract agree_to_call/propose_score_correction already use); the client adapter passes them through (drops the lossy Math.min); the single-param seed_score_match helper keeps its API and derives the two sides itself (seed matches always differ on games). Signatures changed → DROP + CREATE with the prior ACL re-applied (private REVOKEd from PUBLIC; public GRANTed to authenticated + service_role).
  • Classification: migration (deployed) + OTA. Migration 00324 (2 RPCs + the seed helper); client bundle change (adapter match.supabase.ts + regenerated generated.types.ts / apps/web/lib/database.types.ts) → preview OTA.
  • Source commit: 2ad105f1. Migration count 323 → 324. OTA artifact: preview update group 15d1fb55-6484-4d2c-89e0-7772be15c5fc (Android 019f30ec-2edf-7732-99e6-f783a2e999eb, iOS 019f30ec-2edf-7a87-ad36-c432a187de7a), runtime 0.6.2. Baseline lastCompatiblePreviewOtaGroup advanced; source/evidence stay pinned to build 255bdfa0 (native-sensitive diff vs b28a60ad empty).
  • Verification: yarn typecheck + yarn lint:strict green. Proven end-to-end vs the running local DB in rolled-back scenarios — 6/6 — via the real public.submit_match_score RPC as an admin — plus a committed pgTAP test (supabase/tests/00020_two_sided_tiebreak_submit.test.sql, 4 assertions): a 6-6 with team1 winning the tiebreak stores 7/5 UN-swapped (pre-fix: 5/7) and rates team1 the ELO winner; the inverse (5/7) rates team2; seed_score_match's single-param API derives 7/6 on a 7-6 games win and still rates the games winner. Blast radius traced: the only client caller is the adapter (updated); seed-scenario calls seed_score_match (single-param, unchanged); the gen-type edits were made surgically (matching the generator's exact field order) rather than by overwriting from local (which lacks some intermediate migrations). Adversarially reviewed (independent agent): no blockers; three should-fixes applied pre-ship — (1) a matches CHECK ((team1_tiebreak_score IS NULL) = (team2_tiebreak_score IS NULL)) + a client both-or-neither guard in match-card.tsx, closing a narrower same-class hole where a one-box admin input would crown that side the winner (verified: asymmetric input now raises check_violation); (2) seed_score_match leaves both sides NULL on a tied-games seed (was arbitrarily assigning team1; cosmetic — the draw branch ignores the tiebreak; verified both-NULL); (3) narrowed the public grant to authenticated only (the sole prior explicit grant — service_role was an unrequested widening).
  • Rollback pointer: revert 00324 — DROP + restore the 00317 private body + the 00123 public wrapper + the 00091 seed_score_match; revert the adapter + gen-type edits + republish the prior OTA. Deferred (documented): the dead, unwired client elo.rules.ts preview engine still uses games-only winner logic (zero consumers) — fix when a rating-preview feature is wired.

2026-07-05 Backend 00323 — audit remediation batch 14 (tiebreak-aware ELO winner)

  • Change: fourteenth audit batch. private.apply_match_ratings (00236) determined the match WINNER from GAMES ONLY (v_t1 > v_t2) and never read team1/2_tiebreak_score. So a set with TIED games decided by the tiebreak — 6-6 (7-5), reachable via the scorecard's score-edit steppers + optional tiebreak and correctly stamped outcome='decisive' by submit_match_score/agree_to_call (00317) — fell to the ELSE branch and rated the tiebreak WINNER as a FULL LOSS (v_actual_a := 0.0), recorded the inverse in player_matchups W/L, and mis-computed the upset flag. A real ELO-accuracy bug.
  • Fix: read the tiebreak columns, derive v_t1_wins/v_t2_wins ONCE (games, broken by the tiebreak when games are level), and use them at the four WINNER sites — the decisive + incomplete actual-score branches, upset detection, and the four player_matchups W/L expressions. The MARGIN math (v_total_games/v_game_pct/score_factor) stays games-based on purpose (a tiebreak set is a narrow win — no dominance multiplier). A truly level result (equal games AND equal/no tiebreak) still resolves to 0.5. Function reproduced verbatim from 00236 apart from these edits (CREATE OR REPLACE preserves OID + ACL).
  • Classification: migration (deployed), backend-only. Migration 00323 — 1 function (apply_match_ratings); no client bundle change, no OTA.
  • Source: migration 00323_audit_batch14_tiebreak_elo_winner.sql (committed with this entry). Migration count 322 → 323.
  • Verification: reproduction fidelity confirmed by a line-by-line diff of the new function body against 00236 (only the 10 intended edits differ — no dropped lines, constants, or logic drift). Proven end-to-end vs the running local DB (real engine) in rolled-back scenarios — 5/5 — plus a committed pgTAP test (supabase/tests/00019_tiebreak_elo_winner. test.sql, 4 assertions, passes via supabase test db): a 6-6 (7-5) rates team1 the winner (gains ELO — pre-fix it LOST), 6-6 (5-7) rates team2, a 7-6 GAMES control is unchanged, and player_matchups records 1/0 for the tiebreak winner. Adversarially reviewed (independent agent) given the ELO-engine blast radius: reproduction confirmed byte-faithful via diff -u, win-boolean logic proven correct for all 6 cases, and the review noted a beneficial side effect — the 2-arm→3-arm decisive branch also fixes a live bug where a genuine tie submitted outcome='decisive' fell to ELSE 0.0 (auto full-loss for team1), now 0.5.
  • The review caught a scope gap this migration alone does NOT close (tracked as the immediate fast-follow, batch 15 / migration 00324): apply_match_ratings now reads the tiebreak correctly, but on the admin score-entry path submit_match_score writes it side-swapped for tied games — the adapter reduces the two per-team tiebreak inputs to a single Math.min(...) (match.supabase.ts) and the RPC re-derives the two-sided columns from GAMES only, so a 6-6 always hardcodes team2 as the tiebreak winner. So after 00323 the tiebreak-winner bug is fully fixed on the live-consensus path (agree_to_call passes two-sided params) and half-fixed on the admin path until 00324 changes submit_match_score's signature to accept two-sided tiebreak scores. 00323 is still a strict improvement (net-better on the admin path, fully-correct on consensus + the true-tie ELSE bug) and safe to ship first.
  • Rollback pointer: revert 00323 — restores the 00236 apply_match_ratings body (CREATE OR REPLACE, no data migration). Already-applied ratings are not retroactively recomputed (historical), so a revert only changes future ratings.
  • Known scope: only mends future ratings — historical matches scored with tied games + tiebreak keep their (wrong) applied deltas unless private.recompute_all_ratings() (00237) is separately rerun; a backfill is a deliberate out-of-scope decision. The dead, unwired client elo.rules.ts preview engine keeps the old games-only logic (zero consumers) — to be fixed with 00324's client changes before any rating-preview feature is wired.

2026-07-05 Backend 00322 — audit remediation batch 13 (release future seats on leave)

  • Change: thirteenth audit batch (Area 4 of the club-lifecycle audit). Leaving / being removed from a club is a bare DELETE on club_members (RLS: is_club_admin OR self; guarded only by prevent_last_owner_removal) with NO cleanup — so a departed member's confirmed RSVP keeps holding a seat on upcoming sessions (can keep the session at capacity / locked, blocking others) and any unpaid payment hold lingers. attendance_ records, completed-session payments, per-club ELO, and unpaid dues are history / real debt and stay intact.
  • Fix: an AFTER DELETE trigger private.release_member_future_seats_on_leave (SECDEF) that, for a LIVE club (cascade-skipped during a purge_archived_clubs club-delete, since the parent row is gone and sessions/rsvps cascade away too — the same idiom as 00319/00311), releases the departing member's FUTURE confirmed/waitlisted RSVPs. It mirrors cancel_session_as_host: pre-seed an excused attendance_records row FIRST, then UPDATE the RSVP → cancelled. The pre-seed makes classify_rsvp_cancel (00127) preserve excused instead of scoring a no_show/late_cancel STRIKE — a member must not be penalised for leaving — while the →cancelled UPDATE still fires the autolock re-open (frees the seat) and the payment-sync (drops the unpaid hold).
  • Classification: migration (deployed), backend-only. Migration 00322 — 1 trigger; no client bundle change, no OTA.
  • Source: migration 00322_audit_batch13_release_seats_on_leave.sql (committed with this entry). Migration count 321 → 322.
  • Verification: proven end-to-end vs the running local DB (real triggers, incl. the live classify/autolock/payment-sync chain) in rolled-back scenarios — 6/6 — plus a committed pgTAP test (supabase/tests/00018_release_seats_on_leave.test.sql, 5 assertions, passes via supabase test db). Future confirmed + waitlisted RSVPs are cancelled on leave; a PAST RSVP is untouched; the near-future cancel records excused/strike 0 (NOT the no_show/1.0 it would score without the pre-seed — the core strike-suppression win); no attendance row is fabricated for the past session; and a club-purge cascade does not error (trigger skips). Cross-validated the fired-trigger chain: signals_on_rsvp_update only emits on waitlist-promotion (→confirmed), so the departed member is NOT notified; and sync_session_payment_on_rsvp drops only the pending hold on →cancelled, leaving a paid hold untouched (matching the scope note below). Adversarially reviewed (independent agent, which empirically reproduced the leave + account-cascade paths against a live DB): no blockers; one code should-fix applied pre-ship — the excused pre-seed is now gated on attendance_tracking_enabled (mirroring cancel_session_as_host), so a tracking-off club gets NO fabricated attendance row (validated: seat still freed, zero attendance rows — a 7th/6th pgTAP assertion). Three scope-accuracy should-fixes documented in the migration header: submitted/attested (송금 완료) holds survive like paid ones; a departing member still seated in a SCHEDULED match roster is a tracked match-gen follow-up; and the trigger targets the leave/kick path (account deletion is soft-delete, handled separately).
  • Rollback pointer: revert 00322 — DROP TRIGGER IF EXISTS + drop the function; no data migration (already-cancelled RSVPs / seeded excused rows stay, which is correct).
  • Scope (documented, not gaps): a PAID hold on a future session (rare — paid then left) is left for the host's off-app refund call, same as the paid-no-show policy; unpaid dues stay a real balance the admin can write off; past attendance / ELO are history.

2026-07-05 Backend 00321 — audit remediation batch 12 (join-policy stranded requests)

  • Change: twelfth audit batch (Area 3 of the club-lifecycle audit). club_join_request rows (00213) are created only under a club's approval join_policy, but join_policy is a freely admin-mutable column (plain clubs_update; not in protect_clubs_columns) and NOTHING fired when it changed. So flipping approval → open (or invite_only) orphaned every pending request: it could never be meaningfully accepted, the applicant couldn't self-clear it (no withdraw RPC + the only club_join_request UPDATE policy is admin-only), and the partial-unique (club_id,user_id) WHERE status='pending' index then PERMANENTLY blocked that user from re-applying if the club later flipped back to approval (apply_to_club RAISEs apply_already_pending on the fossil row).
  • Fix: an AFTER UPDATE OF join_policy trigger (private.resolve_join_requests_on_policy_change, SECDEF) that cancels the now-moot pending requests the instant approval is turned off, plus a one-time backfill for rows already stranded on non-approval clubs. Cancelling (not deleting) keeps the audit trail and frees the partial-unique index so re-application works. (A member-facing "withdraw my application" RPC + button — for the distinct case of an admin who never decides while the club stays approval — is a client-involving follow-up.)
  • Classification: migration (deployed), backend-only. Migration 00321 — 1 trigger + 1 one-time backfill; no client bundle change, no OTA.
  • Source: migration 00321_audit_batch12_join_policy_stranded_requests.sql (committed with this entry). Migration count 320 → 321.
  • Verification: proven end-to-end vs the running local DB (real trigger) in rolled-back scenarios — 5/5. approval→open and approval→invite_only both auto-cancel the pending request; an already-accepted request is untouched by a policy flip; re-application succeeds after auto-cancel (the fossil no longer trips the partial-unique index); and the one-time backfill clears a legacy row stranded on an already-open club. Cross-validated that the only trigger on club_join_request is updated_at (no signal/counter/strike misfires on the system-cancel), and that club_join_request CASCADE-deletes on club purge (so archival stranding self-resolves and is correctly out of this batch's scope). Adversarially reviewed (independent agent): no blockers; two should-fixes addressed — (1) the archival-stranding scope-out is now documented in the migration header as an intentional bounded risk (auto-cancelling on archive would break unarchive/recovery; it self-resolves via the purge CASCADE), and (2) a committed pgTAP regression test (supabase/tests/00017_join_policy_stranded_requests.test.sql, 5 assertions, passes via supabase test db) now locks the trigger behavior — approval→open/invite_only cancels, a decided request is untouched, and re-application succeeds post-cancel. Nits fixed (dropped the redundant updated_at=now() since the BEFORE UPDATE trigger already stamps it; softened the "unclearable" header wording — an admin retained a manual reject path).
  • Rollback pointer: revert 00321 — drops the trigger + function (DROP TRIGGER IF EXISTS / the backfill is a one-time data update, not reverted; already-cancelled rows stay cancelled, which is correct).

2026-07-05 Backend 00320 — audit remediation batch 11 (archived-club membership gates)

  • Change: eleventh audit batch (Area 2 of the club-lifecycle audit; research + live-DB cross-validation). A club soft-archived via archive_club (00214 — sets clubs.archived_at, a 30-day recovery window before the purge_archived_clubs cron hard-deletes) still accepted new members through 2 of 4 add-paths, an inconsistency: join_club_member_checked (00304, the shared core of open-join + invite-join) already RAISEs 'Club is archived', but (a) apply_to_club selected only id/is_discoverable/join_policy — never archived_at — so a member could lodge a join REQUEST on an archived club; (b) decide_join_request checked only admin + pending, then INSERTed into club_members — so an admin could ACCEPT an applicant into an archived club; (c) the club_members_insert RLS policy (00311, admin-direct add) never gated on it. Net: three paths add an active member (roster count + join signal + notifications) to a club scheduled for hard-deletion.
  • Fix: a reusable public.is_club_active(club_id) SECDEF predicate (mirrors the is_club_admin helper family — search_path='', grants to authenticated/anon/service_role); an archived RAISE in apply_to_club + decide_join_request (accept only — rejecting a pending request on an archived club stays allowed for cleanup) matching join_club_member_checked's error; and the predicate ANDed into club_members_insert. Both RPCs reproduced verbatim from their live definitions apart from the added guard (CREATE OR REPLACE preserves OID + ACL, so no re-GRANT needed).
  • Classification: migration (deployed), backend-only. Migration 00320 — 1 new helper
    • 2 RPC updates + 1 RLS policy; no client bundle change, no OTA.
  • Source: migration 00320_audit_batch11_archived_club_join_gates.sql (committed with this entry). Migration count 319 → 320.
  • Verification: proven end-to-end vs the running local DB (real RLS + SECDEF + triggers) in rolled-back scenarios — all pass. is_club_active active→t / archived→f; apply_to_club archived-blocked / active-succeeds; decide_join_request accept-into- archived blocked / reject-on-archived allowed / accept-into-active adds the member; club_members_insert direct-add archived-blocked-by-RLS / active-allowed. Completeness cross-validated: enumerated every club_members INSERT + is_active reactivation path — the only four are handle_new_club (SECDEF trigger, a new club is always active), join_club_member_checked (gated), decide_join_request (now gated), and the club_members_insert policy (now gated); forfeit_session only reads the table. Confirmed the owner-bootstrap (handle_new_club) is SECDEF so club creation is NOT blocked by the new policy gate. Adversarially reviewed (independent agent): no blockers; two should-fixes applied pre-ship — (1) dropped the gratuitous anon EXECUTE grant on is_club_active (a stale would-be existence/archival oracle; 00209/00217 already de-anon'd this corpus) with an explicit REVOKE … FROM anon, and (2) added the FOR SHARE clubs-row lock to apply_to_club + decide_join_request (matching join_club_member_checked) to close a TOCTOU where a concurrent archive_club could land one straggler member.
  • Rollback pointer: revert 00320 — restores the prior apply_to_club / decide_join_request bodies + the 00311 club_members_insert policy and drops the is_club_active helper (CREATE OR REPLACE / DROP POLICY, no data migration).
  • Scope note: content writes (sessions / posts / rsvps / dues) on an archived club are a separate deliberate "freeze" product decision, tracked apart from this membership fix.

2026-07-05 Backend 00319 — audit remediation batch 10 (session-delete lifecycle safety)

  • Change: tenth audit batch (research agent traced the current SQL; I cross-validated against the running schema). The session-delete surface had four compounding defects. (A) Host 일정 삭제 was a silent no-op — b6d5b2d7's cancel-first delete flow deletes a cancelled session, but sessions_delete (00011) permitted DELETE only when status='draft' AND caller is_club_admin, so the delete matched 0 rows and returned no error (false "deleted" toast; row + children survive), and a 번개 host (created_by, not a club admin) was refused even a draft delete. (B) Rewind to draft → CASCADE-wipesessions_update has no transition guard and the client status writer is a raw .update({status}), so an admin could rewind a completed/paid session to draft (never arming the 00307 refund trigger, which only fires on →cancelled) and then delete it, CASCADE-dropping rsvps / session_payments (incl. paid) / attendance / matches. (C) created_by was not immutable — the hole 00311 closed on clubs but never on sessions; an admin could UPDATE sessions SET created_by=<victim> to launder both 00307's refund exclusion and the new delete backstop (both keyed on user_id<>created_by). (D) Solo-only delete invariant was client-JS only. Fix — five layered sections, each mirroring an existing canonical pattern: (1) sessions_delete(is_club_admin OR created_by=auth.uid()) AND status IN ('draft','cancelled'); (2) a BEFORE UPDATE role-aware guard (guard_session_status_transition, invoker, like prevent_last_owner_removal) reverting a client rewind INTO draft or any change off completed; (3) protect_sessions_columns on created_by+club_id via the shared 00074 private.protect_columns (exactly as 00311 did for clubs); (4) a BEFORE DELETE backstop (block_unsafe_session_delete, SECDEF — a pickup host is often not a club member, so INVOKER reads would be RLS-blind) RAISEing on a completed match (ELO) or an other-than-host confirmed RSVP; (5) a cascade-skip retrofit onto 00307's sibling refund-block trigger.
  • Adversarial review found two blockers before ship, both fixed + re-verified: (1) the BEFORE DELETE backstop fired on FK-cascade deletes, so the daily purge_archived_clubs cron (DELETE clubs → CASCADE sessions) would have aborted on any club that ever had a completed match — i.e. essentially every real club. Fixed with the prevent_last_owner_removal cascade-skip idiom (IF NOT EXISTS (SELECT 1 FROM clubs WHERE id=OLD.club_id) THEN RETURN OLD) on both the new backstop and 00307's sibling (which shared the latent defect). (2) the created_by spoof laundering path (defect C above), fixed by section 3. A honest scope note is inlined: the backstop fully protects the new non-admin-host actor; a club admin can still bypass the other-confirmed check by raw-mutating another member's RSVP (they already had that power + delete rights) — the money net (00307 arms on →cancelled regardless) and ELO net (completed-match, rsvp- independent) still hold; closing the admin raw-RSVP path fully is a tracked follow-up (an rsvps.status SECDEF guard mirroring 00312).
  • Classification: migration (deployed), backend-only. Migration 00319 — 1 RLS policy + 4 trigger/function changes on public.sessions; no client bundle change, no OTA.
  • Source: migration 00319_audit_batch10_lifecycle_safety.sql (committed with this ledger entry). Migration count 318 → 319.
  • Verification: proven end-to-end against the running local DB (real RLS + triggers) in rolled-back scenarios — 14/14. Delete authority (6): host deletes own draft ✓ and own cancelled ✓ (the two previously-broken cases), completed stays ✓, non-host/non-admin blocked ✓, admin retains delete ✓, host-own-RSVP doesn't self-block ✓. Backstop (2): completed-match blocked ✓, other-confirmed blocked ✓. Rewind guard (3): completed→draft reverted ✓, open→draft reverted ✓, legit draft→open allowed ✓. Blocker fixes (3): club purge cascades the completed-match session WITHOUT aborting ✓, admin created_by spoof reverts to host ✓, retrofitted 00307 refund-block still fires on a normal delete ✓. Cross-validated no client writes status='draft'; auto_advance is postgres/cron-only. Two independent adversarial-review agents (round 1 found the two blockers; round 2 a focused verify of the fixes).
  • Rollback pointer: revert 00319 — restores the prior sessions_delete body + the original 00307 trigger and drops the additive triggers (CREATE OR REPLACE / DROP TRIGGER IF EXISTS, no data migration).
  • Known behavior (by design, not a gap): deleting a cancelled+settled session CASCADE-drops its already-refunded session_payments rows — the b6d5b2d7 "final purge" intent. Deeper lifecycle gaps from the same audit (archived-club write gates, join-policy stranded requests, ghost rows after leave) plus the admin raw-RSVP guard are tracked as later batches.

2026-07-05 Preview OTA 265d1fc4 — audit remediation batch 9 (pickup create dropped config)

  • Change: ninth batch from the adversarial session-lifecycle audit — the first client-only batch. The pickup (번개) submit branch of the create-session wizard forwarded only 9 basic fields and silently dropped every config the shared wizard collects for 번개 hosts exactly as it does for club sessions: isFriendly (친선 → the pickup was persisted competitive/ELO-affecting), recurrence (a recurring pickup created ONE session, not the series), matchRules, rsvpDeadline, minPlayers, courtCount, rotationMinutes, courtVenueId. All are user-reachable: renderStep() renders Step3 (친선 toggle), Step4 (recurrence — isPickup gates only the fee section), and Step5 (match rules) for pickup too. The submit branch now mirrors the club branch exactly and routes through the existing hostPickupGame usecase (which already loops the recurrence series). HostPickupGameVariables extended to carry the same fields; no new code, parity with the club path.
  • Classification: OTA only (JS-only client change, two files; no migration, no edge fn). Native-sensitive diff vs baseline commit b28a60ad is empty.
  • Source commit: 027a388d. App / runtime version: 0.6.2 (JS-only client).
  • OTA artifact: preview update group 265d1fc4-2f72-48e0-ae29-13dc7fd61b20 (Android 019f302c-6053-744a-9953-5b939fc3452e, iOS 019f302c-6053-7809-b8d3-921675c1e6c9), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn typecheck + yarn lint:strict (0 warnings) + yarn check green; host-pickup-game.usecase + session.mapper suites re-run fresh (41 tests) to cover the widened usecase input. Adversarially reviewed by tracing the wizard step flow: confirmed Step3/Step4/Step5 render for pickup with no isPickup step-skip, so the dropped config was genuinely user-reachable (not a theoretical gap), and the fix is exact parity with the already-correct club branch.
  • Rollback pointer: republish 3719d18f-.... Source: revert 027a388d (a pure-additive pass-through of existing wizard-draft fields).
  • Known gaps: on-device QA of a recurring/친선 pickup end-to-end is not something this run can perform — preview OTA is published; device verification is owner QA.

2026-07-05 Backend 00318 — audit remediation batch 8 (attendance integrity)

  • Change: a 총무's no_show ruling (final_status='no_show', strike 1.0) was silently erased on two paths. (1) GPS self-check-in (check_in_attendance_gps) overwrote any non-'excused' record to 'attended'/0, so a member marked absent could clear their own no-show + strike (evading the ≥2-strike RSVP cooldown) with client-supplied, spoofable coordinates → now treats an existing 'no_show' as authoritative and no-ops. (2) Session completion (session_completed_record_attendance) rewrote every non-'excused' record to 'attended'/0 → now preserves 'no_show' + its strike alongside 'excused'.
  • Classification: migration (deployed), server-only — no OTA. Migration 00318 applied to the linked project via supabase db push --linked.
  • Verification: proven on the local DB — a member with an admin no_show is not erased by GPS check-in (record + strike preserved, no-op returned) and normal check-in still records 'attended'; completing a session preserves a 총무's no_show + strike (was overwritten).
  • Rollback pointer: revert this commit (re-applies the 00136 / 00127 bodies).
  • Deferred (deeper): auto_advance auto-completing a NEVER-STARTED session still awards 'attended' + bumps total_sessions_confirmed for its confirmed members (phantom attendance / trust inflation) — needs a "did the session actually run" signal, a later attendance pass.

2026-07-05 Backend 00317 — audit remediation batch 7 (scoring integrity)

  • Change: (1) a TIED score finalized by consensus was rated as a decisive team-2 win — agree_to_call hard-coded apply_match_ratings(..., 'decisive'), whose branch treats t1=t2 as team1's full loss (0.0). Now derives the outcome from the call scores (draw only when games AND tiebreaks are level — a 6-6/7-5 tiebreak set stays decisive) and stamps matches.outcome. (2) submit_match_score (admin board score) didn't void a pending match_score_calls row, so a late 동의 could overwrite the admin's authoritative score with the stale snapshot — now deletes the pending call (mirrors update_live_match_score). Also fixed a latent SELECT … FOR UPDATE (no INTO) in the finalize path that would raise 'query has no destination' — the consensus finalize had never actually worked.
  • Classification: migration (deployed), server-only — no OTA. Migration 00317 applied to the linked project via supabase db push --linked.
  • Verification: proven on the local DB — a tied (5-5, no tiebreak) consensus now finalizes completed/outcome='draw'; a 6-6 set won 7-5 in the tiebreak stays 'decisive'; a non-tied consensus stays 'decisive'; an admin submit voids the pending call and writes the authoritative score.
  • Rollback pointer: revert this commit (re-applies the 00236 bodies).
  • Deferred (deeper): apply_match_ratings still picks the winner by games only and ignores the tiebreak, so a tiebreak-decided set's ELO credits the wrong team — a core-engine change for a later rating pass; and the board score-EDIT recompute gap (edit of an already-rated match no-ops on the idempotency guard).

2026-07-05 Backend 00316 — audit remediation batch 6 (weather-warning enum)

  • Change: signals_on_weather_warning_activated (fan-out that pushes a severe- weather warning to attendees of at-risk upcoming sessions) filtered s.status IN ('scheduled','published') — neither is a valid session_status label, so Postgres raised 22P02 every time the trigger ran, which aborted the warning-cache upsert AND emitted zero warnings. During any real KR 호우/폭염/한파 event the safety-notification path was dead. Fix: use the real non-terminal labels ('open','locked','in_progress').
  • Classification: migration (deployed), server-only — no OTA. Migration 00316 applied to the linked project via supabase db push --linked.
  • Verification: proven on the local DB — inserting an active weather_warning_cache row (which previously raised 22P02) now succeeds and emits a session_weather_warning signal to a confirmed attendee of an upcoming session.
  • Rollback pointer: revert this commit (re-applies the prior 00287 body).
  • Deferred to the next notification pass: approval-funnel signals, send-push title-key gaps, the re-emit-never-repushes reset, and the global-cap-counts-DM starvation (they touch core signal plumbing / need new i18n — handled together).

2026-07-05 Backend 00315 — audit remediation batch 5 (club_posts privileged columns)

  • Change: the club_posts UPDATE policy had a USING clause but no WITH CHECK, so an author could flip their own post's audience from 'member' to 'public'/'official' (defeating the owner/admin-only publish gate → the post surfaces on the club's public discovery face), reassign club_id into a club they aren't in (cross-tenant injection), promote to type='announcement', pin it, or reassign author_id (impersonation). Fix: a BEFORE UPDATE role-aware column-protect trigger reverts a non-admin's changes to audience/club_id/type/is_pinned and makes author_id immutable for all client writers; RLS restored to USING-only. A WITH CHECK was deliberately NOT used — the INVOKER count-bump triggers UPDATE club_posts as the member, so a whole-row check would reject legitimate reaction/comment/read counts.
  • Classification: migration (deployed), server-only — no OTA. Migration 00315 applied to the linked project via supabase db push --linked.
  • Verification: proven on the local DB (10 rolled-back assertions): non-admin audience-escalation / club_id-injection / type→announcement / pin / author_id reassignment all reverted; author body edits, admin publish, and INVOKER count-trigger updates all still work. Adversarial review (agent) confirmed the 4-column goal solid and surfaced the adjacent author_id impersonation gap, which was closed before shipping; it also confirmed the deferred reaction/poll-vote INSERT gates are safe to defer (non-author reaction/comment counts are already a no-op today via the same count-trigger→RLS path).
  • Rollback pointer: revert this commit + re-apply the original 00041 club_posts UPDATE policy (drop the trigger).
  • Deferred: reaction/poll-vote INSERT membership gates (interact with the pre-existing count-trigger↔RLS behavior — a separate content-stuffing pass).

2026-07-05 Backend 00314 — audit remediation batch 4 (club_member_joined fan-out)

  • Change: signals_on_club_member_insert fanned a 'club_member_joined' signal to every existing member using a recipient-AGNOSTIC dedup_key, so signals_emit's ON CONFLICT (dedup_key) collapsed all but the first member onto one row — on the most common club event, only one member was ever notified. Fix: append the recipient to the dedup_key (the pattern every other fan-out already uses).
  • Classification: migration (deployed), server-only — no OTA. Migration 00314 applied to the linked project via supabase db push --linked.
  • Verification: proven on the local DB — a 4th member joining a 3-member club now produces 3 distinct signals (one per existing member), where the old code produced 1. (Testing also caught an initial schema slip — the function is public., not private. — before it shipped.)
  • Rollback pointer: revert this commit + re-apply the 00192 function body.
  • Deferred from this batch: approval-funnel signals + the invite_code discover-feed exposure — grouped into a later notifications/security pass (the invite-code approval-bypass was already closed in 00311).

2026-07-05 Backend 00313 — audit remediation batch 3 (participant_ids + payment safety)

  • Change: (1) matches.participant_ids was never populated on app-created matches (mapper omits it; default '{}') — new BEFORE INSERT/UPDATE-of-team-cols trigger derives it as the single source of truth (sorts after protect_matches_scoring) + one-time backfill. (2) A 송금 완료 (submitted) hold was silently DELETEd on RSVP cancel — now deleted only when submitted_at IS NULL (attested holds survive; the host reconciles via 정산). (3) The idle-expiry cron locks (rsvps → session_payments order) + re-checks the hold before evicting, so a member who attests mid-sweep isn't cancelled.
  • Classification: migration (deployed), server-only — no OTA. Migration 00313 applied to the linked project via supabase db push --linked (backfill populated participant_ids for existing matches). No client change (the score-verification home count reading participant_ids has no UI consumer, so the backfill can't flood users).
  • Source commit: cd6bc774.
  • Verification: proven on the local DB — participant_ids populates on insert + syncs on a swap despite the protect trigger; attested hold survives cancel while an idle hold is released; a paid hold still refunds on cancel; the cron still evicts an idle expired hold. Adversarial review caught two real design errors fixed BEFORE shipping: an auto-refund for submitted holds (phantom obligations + a permanent host ding — dropped, as 'submitted' is a self-attestation not confirmed receipt) and an ABBA deadlock in the cron lock order (fixed to rsvps→session_payments).
  • Rollback pointer: revert cd6bc774 + re-apply the prior 00299/00301 bodies.

2026-07-05 Preview OTA 3719d18f + backend 00312 + edge fn — audit remediation batch 2 (match start + RSVP capacity)

  • Change: second batch from the adversarial session-lifecycle audit. Two verified blockers. (A) Match start was silently reverted — moving a match scheduled→in_progress was a raw authenticated .update() of protected columns (protect_matches_scoring reverts it with no error), so no match ever reached in_progress on a real device and live scoring was unreachable. New SECDEF RPC public.start_match (mirrors submit_match_score; authorizes session HOST via created_by OR club owner/admin/match_director — the host branch mirrors the 00303/00307 host-vs-admin split); match.supabase.ts routes through it; the inert start-round CTA (wrong "current round" + a render gate mutually exclusive with its own visibility gate) is fixed and now shows for the host too. (B) RSVP overbooking / dead waitlist — a capacity gate added to the existing assign_waitlist_position trigger demotes a confirm on a full session to waitlisted; a new autolock trigger sets open→locked at capacity and locked→open when a seat frees (makes the built waitlist UI reachable), with a guard on alert_session_created so the re-open does NOT re-announce the session to the club. seed-scenario caps the club_growth roster at max_players.
  • Classification: migration (deployed) + edge fn (deployed) + OTA. Migration 00312 applied to the linked project (llhvhhpbjnnrfwcczizc) via supabase db push --linked; seed-scenario edge function redeployed; the app bundle change (adapter RPC rewire, live-tab CTA, regenerated generated.types.ts for start_match) is JS-only (native-sensitive diff vs baseline commit b28a60ad is empty).
  • Source commit: d10f4bdd. App / runtime version: 0.6.2 (JS-only client).
  • OTA artifact: preview update group 3719d18f-2884-43ee-8238-dfaebe8a8142 (Android 019f2fcd-f381-7650-8faf-d15f36e9c0d7, iOS 019f2fcd-f381-7bf1-800b-1be891e1c170), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn typecheck + full yarn test (2,881) + yarn lint:strict (0 warnings). **Migration proven end-to-end against the local DB (real triggers
    • RLS) in rolled-back scenarios:** start_match starts a scheduled match (SECDEF bypasses the protect trigger; a raw client update stays reverted), is idempotent, authorizes host + admin + match_director and blocks a non-host non-admin; capacity gate demotes the over-cap confirm to waitlisted(pos 1) while auto-lock toggles open↔locked; the locked→open re-open posts NO new club activity alert (guard verified even with the 1h dedup bypassed). Adversarially reviewed twice (6-agent sweep found 2 real issues → fixed; 2-agent re-verify confirmed both resolved).
  • Rollback pointer: republish 90326adb-.... Source: revert d10f4bdd; the RPC + trigger changes are additive/CREATE OR REPLACE (restoring the prior assign_waitlist_position body + dropping start_match/autolock reverts).
  • Known gaps (documented, not blocking): auto-unlock re-opens the seat rather than auto-promoting the top waitlister (promotion still via the expiry cron / manual); the match_director can't-create-session authority gap (audit findings #10/#27) is a separate later batch.

2026-07-05 Backend 00311 — audit remediation batch 1 (5 server-side blockers)

  • Change: first batch from the adversarial session+club lifecycle audits. Migration 00311, five verified blockers: (1) guest-application approval crashed with 42703 (INSERT of a non-existent rsvps.created_at) → dropped the phantom column from both approve bridges; (2) a club could be permanently orphaned → BEFORE DELETE last-owner guard on club_members; (3) a non-owner admin could disband/hijack a club via raw clubs column writes → reused the shared protect_columns to guard archived_at/created_by/type/invite_code/host_id; (4) owner-mint escalation → club_members_insert forbids inserting role='owner' unless the caller is an owner; (5) invite-code join broken for ~98% of clubs (lowercase-hex vs upper() needle) + an approval-bypass → case-insensitive match
    • reject code-join on approval/non-club rows (fixed the preview sibling too).
  • Classification: migration (deployed), server-only — no OTA. Migration 00311 applied to the linked project via supabase db push --linked; no app bundle change, so no OTA artifact.
  • Source commit: d22a5ef0. No native/runtime change.
  • Verification: yarn check (typecheck + tests + docs) green. Proven end-to-end against the local DB (real RPC + triggers) in 14 rolled-back assertions: guest approve/auto both create the RSVP (no 42703); admin-kick-owner + sole-owner self-delete blocked while whole-club CASCADE + account-delete cascade proceed; archived_at client write reverted while the archive RPC succeeds; admin owner-INSERT blocked; uppercased needle matches the lowercase code and approval clubs reject code-join. Adversarially reviewed (7 agents, all canonical, no surviving regressions).
  • Rollback pointer: revert d22a5ef0 + re-apply the prior 00206/00304 bodies to the linked DB (all changes are CREATE OR REPLACE / additive triggers/policies).

2026-07-04 Preview OTA 90326adb + backend 00310 — forfeit auto-substitute + host step-out vs participant forfeit

  • Change: wired the live-session participant actions end-to-end, closing the deferred "Phase 2/3" gaps in the forfeit RPC. forfeit_session (migration 00310, supersedes 00206) now auto-substitutes the withdrawing player out of every live + upcoming match one round at a time (bench-confirmed first, then a best-effort waitlist promotion), keeping the protected matches.participant_ids cache in sync — which is why it must be the SECDEF RPC, not a client .update() (the 00067 protect_matches_scoring trigger reverts a plain authenticated write to participant_ids). No sub + a nullable (player2) slot frees the player; a NOT NULL player1 slot is left for a manual host reshuffle. The RPC auto-detects the caller: a HOST (created_by) stepping out keeps hosting and the admin signal is suppressed (client renders a distinct neutral 경기에서 빠지기 CTA); a participant forfeit still notifies admins, now excluding the actor. Client: the forfeit hook invalidates matchKeys.bySession so the roster + draw reflect the substitute at once. Also hid the in-app 결제하기 (PortOne) rail for real users (mock adapter + dev-gated dev_mock_pay_session confirm) — dev-only now; the manual 송금 완료 rail is the sole primary session-fee CTA.
  • Classification: migration (deployed) + OTA. Migration 00310 applied to the linked project (llhvhhpbjnnrfwcczizc) via supabase db push --linked; the app bundle change (client CTA split, i18n, forfeit hook, payment-button gate) is JS-only. No native/runtime change (native-sensitive diff vs the baseline source commit b28a60ad is empty).
  • Source commit: 658e2060. App / runtime version: 0.6.2 (JS-only client).
  • OTA artifact: preview update group 90326adb-e22b-41a5-9f54-829ed653f597 (Android 019f2c3a-302f-7cd8-8be8-d8d06e31ec4a, iOS 019f2c3a-302f-7801-b554-5cf27604279a), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn typecheck + full yarn test (2,880+ across app/eslint/clubs, after an i18n en-copy fix) + yarn lint:strict (0 warnings) + docs:generate. Migration proven end-to-end against the local DB (real RPC + all triggers) in three rolled-back scenarios: (1) participant forfeit with a bench → the slot takes the bench player, participant_ids synced (forfeiter out, sub in), RSVP cancelled, attendance excused/strike-0, admin signal to the host-owner excluding the actor; (2) host step-out with the host in a NOT NULL (player1) slot → bench substitute fills it, RSVP cancelled/excused, and the forfeit signal is suppressed (0 rows); (3) full session, no bench/waitlist, forfeiter in a nullable slot → nulled cleanly, participant_ids drops them, no error.
  • Rollback pointer: republish 0dbead44-.... Source: revert 658e2060 (client) + re-apply the 00206 forfeit_session body to the linked DB (the 00310 CREATE OR REPLACE is additive; restoring 00206 reverts the RPC).
  • Known gaps: the residual NOT NULL-player1 + no-sub case is left for a manual host reshuffle by design (not a full rotation regeneration); real PortOne checkout is still deferred (Phase B — real adapter + verify swap + EAS build). On-device preview QA of the host/participant CTA split + substitution UX is the next proof.

2026-07-04 Edge deploy 01498783 — dev seed-scenario data richness + 3 silent-bug fixes

  • Change: made dev scenarios mimic real life + fixed bugs the deep-dive surfaced (all schema-verified against real migrations). Fixed generateSessionPayments (queried a non-existent sessions.cost_per_person → dead; also wrote Korean into payment_method, violating the 00285 vocab CHECK) → now uses participation_fee + real vocab + the method/submitted_at split (00301); fixed guest_applications missing a NOT NULL club_id (00017) that failed silently every run; fixed DM unread always 0 (computed from last_read_at, 00289, and seeded threads were auto-read at creation) by rewinding last_read_at. Added: a cancelled fee session driving the real refund trigger (00307 refund_state pending/acknowledged), club_join_request accepted/rejected variety (00213), DM request_status pending/declined, partial read-state for club_alert_reads/club_post_reads. All state mixes deterministic (index % N).
  • Classification: backend edge fn (deployed), no app-bundle change → no OTA. Companion to the perf entry below (0dbead44); both are the seed-scenario edge function shipped via supabase functions deploy.
  • Source commit: 01498783.
  • Backend deploy: npx supabase functions deploy seed-scenario --no-verify-jwt to the linked project (llhvhhpbjnnrfwcczizc). No migration (uses existing schemas/triggers).
  • Verification (THOROUGH, at scale): eslint clean + deploy bundle valid + live runs. session_fee_manual (2 sessions) + club_growth (joinRequests=7) + home_pickup all seed clean. The full dense chain proved it: dense_social now reports sessionPayments=904 (was silently 0 — the dead generator) and readState=258 (was 0 — never seeded), with all 6 changes executing clean (no CHECK/NOT-NULL errors); dense_matches seeded 1,472 matches, dense_compute scored 1,000 with matchErrors=0.
  • Rollback pointer: source revert 01498783 + redeploy the prior edge fn.

2026-07-04 Preview OTA 0dbead44 + edge deploy — dev seed-scenario speedup (batch matches + reconcile-once)

  • Change: the dev seed-scenario tool was slow because matches were inserted one row per HTTP round-trip in nested loops (the team had batched match_sets after it "blew the wall-clock" but never applied it to matches), and dev_reconcile_seeded_world (a full-DB rating replay) ran after EVERY chain step. Fixes: (1) batch the matches inserts — createSessions + seedLiveSession accumulate rows and do ONE .insert(rows).select('id'), mapping ids back to their match_sets (mirrors the existing set-batching); single-match home_* sites stay single. (2) skipReconcile flag (mirrors skipTruncate) — the dev-panel + run-scenario.mjs pass it on every chain step except the last, so the reconcile + finalization tail runs once over the full dataset instead of O(steps × dataset). dense_compute's deliberate serial scoring untouched (avoids a real ELO race). Removed an orphaned per-row helper.
  • Measured: live_showcase 25.6s → 13.9s (~46%) single-scenario (data verified intact: 16 sessions / 206 matches); chains gain more from reconcile-once.
  • Classification: edge fn (deployed) + OTA. The seed-scenario edge function was deployed to the linked project (llhvhhpbjnnrfwcczizc); the only app-bundle change is the dev-panel skipReconcile client (dev tool). No native/migration.
  • Source commit: 3778e4ea. App / runtime version: 0.6.2 (JS-only client).
  • OTA artifact: preview update group 0dbead44-0b47-4b60-aa62-5a7abd8903a7 (Android 019f2abc-a1c1-7545-90fc-84c3ef29ce8b, iOS 019f2abc-a1c1-7ca9-8ddb-d31692906bde), branch preview. Baseline advanced.
  • Verification: app typecheck + eslint (incl. the edge fn) + the deploy bundle + a live before/after run-scenario measurement. Phase B (seed data richness) next.
  • Rollback pointer: republish 8a0e708f-.... Source: revert 3778e4ea (+ redeploy the prior edge fn if desired — the batching is behavior-identical, so a rollback is only cosmetic).

2026-07-04 Preview OTA 8a0e708f — direct score-correction on the match row (phase 5, redesign complete)

  • Change: score-correction was a 3-tap chain (row → MatchProfilesSheet → MatchActionSuite's 점수 입력 → ScoreCorrectionSheet). A 점수 수정 pill now sits in the completed row header's timing slot (Pressable + Pencil, $primary, COMP-3 compliant), gated by the existing canCorrect, calling openCorrection directly (1 tap) with stopPropagation so the whole-row → profiles-sheet path stays intact. Reused the previously-unused matchActions.correctScore key (점수 수정 / Fix score). No change to propose/consent, gates, or ELO recompute. This completes the 5-phase session-detail button redesign.
  • Classification: OTA-safe JS/TS — packages/features/sessions (completed-row) + i18n. No native/migration change.
  • Source commit: 98d5376b. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 8a0e708f-2966-4740-9f61-a221c7ce8658 (Android 019f28ba-aa5b-7754-9d07-abef63f7865c, iOS 019f28ba-aa5b-7d8d-b2a0-f1a63518f0b6), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + full yarn test (2,286) + lint:strict (0/0).
  • Redesign summary (phases 1–5, source commits): phase 1 d41e87cc (canonical labels/affordance), phase 2 97484a51 + migration 00309 (chat find-or-create), phase 3 731b2edb (host lifecycle band CTA), phase 4 6fc7b3d6 (flatten the 일정 관리 hub inline + kebab removal), phase 5 98d5376b (direct 점수 수정).
  • Rollback pointer: republish 3a2b9051-.... Source: revert 98d5376b.

2026-07-04 Preview OTA 3a2b9051 — flatten the session-management hub (phase 4)

  • Change: dissolved the 일정 관리 hub screen — its actions render inline on session-detail (status-gated), removing a navigation layer. New inline 호스트 도구 SectionBlock (host/manager) reusing the extracted SessionNavRow: 일정 수정 / 참가자 관리 / 경기 규칙 / 호스트 관리 / 출석 확인 / 경기 종료 (confirm) — every gate/route/mutation copied verbatim; 경기 보드 + 정산 not duplicated (they're the host band CTA, Phase 3). Removed the [일정 관리][primary] band manage-pairing (band shows only the single primaryAction). Removed the header kebab (SessionActionsMenu deleted): GPS 출석 체크 + 기권 render inline while in_progress + confirmed participant. edit-session-screen stripped to config-only (일정 수정) + a stale-deep-link fallback; NavRow lifted to shared session-nav-row.tsx. i18n: sessionToolsTitle, editDetails, editNotAllowedTitle.
  • Classification: OTA-safe JS/TS — packages/features/sessions + packages/ui (header-actions-menu doc) + i18n. No native/migration change.
  • Source commit: 6fc7b3d6. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 3a2b9051-41bc-4704-bc58-f22721e98163 (Android 019f28a3-168a-7b55-be09-b94e05518e7e, iOS 019f28a3-168a-7521-b134-7c228818676c), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + full yarn test (2,286) + lint:strict (0/0, incl. no-button-in-card). Phase 5 (score-correction from the match row) pending.
  • Rollback pointer: republish 166138a4-.... Source: revert 6fc7b3d6.

2026-07-04 Preview OTA 166138a4 — host lifecycle band CTA (phase 3)

  • Change: the host's single BottomCtaBand primary is now the state-contextual lifecycle action (reusing existing hooks/routes): open → 초대하기 (handleShare / useShareSession), locked → 경기 시작 (confirm-gated useStartSession, mirrors match-board), in_progress → 경기 보드 (routes.matchBoard, takes precedence over the participant scorecard), completed + hasPaidParticipants → 정산 (routes.sessionPayments) else a host who played a free session still falls through to 매너 태그. 일정 취소 relocated out of the band into the danger zone (with 일정 삭제). The 일정 관리 manage-pairing + refund override are unchanged (Phase 4 removes the hub). Added sessionDetail.invite; other labels reused.
  • Classification: OTA-safe JS/TS — packages/features/sessions + i18n. No native/migration change.
  • Source commit: 731b2edb. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 166138a4-4e3f-43dd-a790-2915e089a2a2 (Android 019f2883-b293-7f0a-bdbc-35146bd8b6eb, iOS 019f2883-b293-7045-891e-0c9c148c6edf), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + full yarn test (2,286). Phases 4 (hub flatten) + 5 (score-row) pending.
  • Rollback pointer: republish 54de47f7-.... Source: revert 731b2edb.

2026-07-03 Preview OTA 54de47f7 + backend 00309 — session group chat find-or-create (phase 2)

  • Change: the session-detail group-chat icon created a NEW group DM every tap (documented gap — dm_threads had no session link). Now find-or-create. Migration 00309_dm_thread_session_link.sql: dm_threads.session_id FK + partial unique index (dm_threads_session_group, ≤1 group thread per session); create_group_thread becomes a true get-or-create (reuses the session's thread, folding in newly-confirmed members with no rate-limit/min-member gate, else inserts with session_id; byte-identical when no session context). SECURITY DEFINER + search_path='' + REVOKE/GRANT preserved. Port/adapter getThreadBySession + useThreadBySession hook + dmKeys.threadBySession + create-group invalidation. startGroupChat → existing thread routes.dmThread, else the (now idempotent) compose flow.
  • Classification: backend + OTA. The migration deploys to the linked DB (backward-compatible: nullable column + no-op RPC path when no session context); the client change is OTA-safe JS. No native change.
  • Source commit: 97484a51. App / runtime version: 0.6.2 (JS-only client).
  • Backend deploy: npx supabase db push --linked applied 00309 to the linked project (llhvhhpbjnnrfwcczizc) — "Finished supabase db push". Generated types regenerated + committed; check:supabase-types OK.
  • OTA artifact: preview update group 54de47f7-3d53-44a2-a01d-f12706266893 (Android 019f286a-59fd-7a44-827f-058e4628b387, iOS 019f286a-59fd-7592-880c-5939f203af8a), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: supabase db reset (migration clean) + yarn check (2,286) + lint:strict (0/0). Migration count is now 309; README + current-project-state regenerated. Phases 3–5 pending.
  • Rollback pointer: republish dbc4f1e7-...; source revert 97484a51. NOTE: the migration is additive/backward-compatible — a client rollback is safe without a DB rollback (the column/RPC-reuse are simply unused by the old client).

2026-07-03 Preview OTA dbc4f1e7 — session-detail button redesign phase 1 (canonical labels + affordance)

  • Change: first slice of the session-detail button redesign (no structural flattening yet). Host-approved band CTA splits the explanation out of the button label — helper "호스트가 승인했어요" ($primary) above a clean "참가 확정하기" button (matches the app's cooldown/tier-gate pattern; hostApprovedConfirmCtahostApprovedNotice + confirmRsvp). Pending-applicants banner tightened (신청 N건 확인). 번개 취소 gets a dedicated "취소 중..." loading string. The 문의 pill's tokens pinned to the canonical secondary+sm+pill values — and it STAYS a Pressable, not a Button, because @twomore/no-button-in-card (COMP-3) forbids a Button inside a Card, so a styled Pressable is the canonical in-card CTA (this corrected the wireframe's "normalize to Button" note). Variant audit found all existing Button/ActionButton emphases already correct.
  • Classification: OTA-safe JS/TS — packages/app (i18n) + packages/features/ sessions (session-detail + pickup-host-card + host-dashboard). No native change.
  • Source commit: d41e87cc. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group dbc4f1e7-5ea9-437a-840c-6e292d93c92d (Android 019f2846-df2c-70f5-b352-7dc9a5aea2d4, iOS 019f2846-df2c-7c3a-8f0b-4e44c5dbb4fd), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + full yarn test (2,286). Phases 2–5 (chat find-or-create, host lifecycle CTA, hub flattening, score-row) pending.
  • Rollback pointer: republish 2eb45a5a-ee98-4efa-b7b8-dc7e0a82f557. Source: revert d41e87cc.

2026-07-03 Preview OTA 2eb45a5a — calendar dots cover past sessions too

  • Change: the DayCalendarPicker session dots were upcoming-only (past months never lit up). Added a purpose-built query for the days a user has a session they're part of — past attended OR future scheduled (confirmed/no_show RSVPs) — in a date range: new rsvp port + adapter findParticipatingSessionDates (distinct sessions.date via the same sessions!inner(date) embed + .gte/.lte the ranged findPlayedSessions uses), a useMyParticipatingSessionDates hook (registry-backed, realtime freshness) + rsvpKeys.participatingDates. The day view's browsed-month dot source swaps the upcoming-only query for the new one; the day-scoped Scheduled-section query is untouched.
  • Classification: OTA-safe JS/TS — packages/app (port/adapter/hook/keys/barrel)
    • packages/features/home (view + picker doc). No migration/RLS/native change.
  • Source commit: e974d057. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 2eb45a5a-ee98-4efa-b7b8-dc7e0a82f557 (Android 019f2806-e688-7038-9e2b-a015802d8de7, iOS 019f2806-e688-744f-ae3e-859e81679d06), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + full yarn test (2,286).
  • Rollback pointer: republish a3a1c80a-206b-4875-8a9b-179edd1f9af8. Source: revert e974d057.

2026-07-03 Preview OTA a3a1c80a — flag audit close-out + calendar session dots + auto-collapse pickers

  • Change: two commits. (1) c31f27db refactor: the flag audit's last real item — record-detail DaySummaryCard still hand-rolled the donut + legend that its sibling OutcomeSummaryCard already delegates to the shared RecapOutcomePanel; migrated it (donut + 3-line hero + full-width stat strip, SummaryChip grid preserved) and removed the dead LegendItem + SegmentedDonutChart import — the last duplicated donut block is gone. (2) eb450287 home period-nav: a new pickAnchor(anchor) store action jumps the anchor AND closes the scrubber, so a picker selection (day/week/month) auto-collapses the panel while the arrows keep it open; and DayCalendarPicker now marks days that have a scheduled (RSVP'd, upcoming) session with a dot — derived from the already-cached upcoming-sessions query (pure re-filter, no extra network), rendered in the same reserved marker line as 오늘 so the grid never resizes.
  • Flag-audit result (rigorous verification): the previously-flagged pickup-session RLS visibility gap is a FALSE ALARM — sessions.club_id is UUID NOT NULL (schema + Zod clubId: uuid()); pickup/번개 sessions still belong to a club, so there are no club_id IS NULL rows and the club-membership RLS is correct. No migration needed. (Only real edge: losing a club's history after you leave it — by-design.)
  • Classification: OTA-safe JS/TS — packages/app (store + i18n unaffected) + packages/features/{home,records}. No migration/RLS/native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commits: c31f27db + eb450287. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group a3a1c80a-206b-4875-8a9b-179edd1f9af8 (Android 019f27d6-e6eb-7705-8b05-622703e9498b, iOS 019f27d6-e6eb-7a60-951e-6b1a90b49473), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + full yarn test (2,285, incl. a new pickAnchor test).
  • Rollback pointer: republish ded35b6a-3aee-4421-bbba-65063a6389c3. Source: revert eb450287 + c31f27db.

2026-07-03 Preview OTA ded35b6a — Phase 2c-B: navigable 오늘/day view + DayCalendarPicker (home period-nav epic complete)

  • Change: completes the home period-nav axis — the 오늘/day view now scrubs by date like week/month instead of being pinned to today. findActiveAndRecent, useActiveAndRecentSessions, and useTodayRecapSessions accept an optional targetDate; HomeTodayView reads the store anchor (mirroring week/month) and renders a PeriodNavPanel + a new DayCalendarPicker scrubber (‹ 년·월 › month stepper over a Sunday-first calendar grid; Korean weekend colors $error Sun / $calendarSat Sat, muted spillover, selected-day fill, a reserved 오늘 marker line per cell — same anti-resize idiom as the month picker). The Live section is gated to isHomePeriodNow('day', anchor) — "in progress now" never follows a past/future anchor; scheduled + recap follow the anchored day. Also fixes a latent UTC bug in the adapter's 2-day lookback (toISOString().slice → local addDays), and makes sessionKeys.activeAndRecent dated (per-day cache) with an activeAndRecentAll(userId) prefix key for start-session invalidation (session-provider + use-start-session updated).
  • Classification: OTA-safe JS/TS — packages/app (adapter/port/hooks/keys/ provider/mutation + i18n) + packages/features/home (HomeTodayView + DayCalendarPicker). No migration/RLS change. No native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: 01e0e7a1. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group ded35b6a-3aee-4421-bbba-65063a6389c3 (Android 019f27af-befe-740d-8709-a65e0d7616e3, iOS 019f27af-befe-73f7-8ccf-9e927a7eb910), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck (app/home/mobile/web) + yarn lint:strict (0/0) + full yarn test (2,285). Home period-nav epic (Phases 1, 2, 2c-A, 2c-B) now feature-complete on preview; on-device QA of day/week/month scrubbing + the pickers is the remaining check.
  • Rollback pointer: republish ae89e06f-7755-4186-a066-1be57dd55023. Source: revert 01e0e7a1.

2026-07-03 Preview OTA ae89e06f — picker "now" markers + stable panel height + Phase 2c-A server-ranged match history

  • Change: two commits. (1) 889fdf16 home period-nav polish: PeriodNavPanel gets a fixed row height (TOUCH_TARGET.MIN) so the inline "on now" marker chip toggling no longer resizes it; WeekMonthPicker + MonthYearPicker now flag the cell/row that IS the current period with a 이번 주 / 이번 달 marker (week rows = inline chip, month cells reserve a fixed marker line so the grid never resizes). (2) 58b71504 Phase 2c-A: useMatchHistory gains optional {from, to}; the rsvp adapter findPlayedSessions embeds sessions!inner(date) and filters server-side (.gte/.lte) when ranged — the no-range path stays byte-identical (H2H/uncapped unaffected). {from,to} threaded through the port + both query keys; the week/month recap now fetches its real range and drops the client-side date filter, so navigating far into the past no longer under-counts. No migration/RPC/RLS change. (Pre-existing, flagged out of scope: a possible pickup-session club_id IS NULL RLS visibility gap in the shared select policies — unchanged by this edit.)
  • Classification: OTA-safe JS/TS — packages/app (adapter/port/hook/keys + PeriodNavPanel) + packages/features/home (views + both pickers). No native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commits: 889fdf16 + 58b71504. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group ae89e06f-7755-4186-a066-1be57dd55023 (Android 019f278d-09fe-7148-97bd-84f4938616df, iOS 019f278d-09fe-78ce-ab85-b3c0c5cee997), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + full yarn test (2,285). Phase 2c-B (day navigation) still pending.
  • Rollback pointer: republish 7b7ab6e6-a340-4760-9a44-ad9fc63b1053. Source: revert 58b71504 + 889fdf16.

2026-07-03 Preview OTA 7b7ab6e6 — WeekMonthPicker + canonical nav arrows + granularity titles/pills

  • Change: home period-nav refinements (follows 6ff5231a). (1) PeriodNavPanel + both pickers' arrows get the canonical bare icon-button treatment — accessibilityRole="button" + hitSlop={8} (~44pt HIG target, no layout footprint, matching HeaderIconButton) so a near-edge tap steps the period instead of mis-firing the center toggle. (2) New WeekMonthPicker (parallel to MonthYearPicker): a year·month stepper over selectable week rows (Mon–Sun ranges); tapping a week sets the anchor — the week panel now taps to open it. (3) Titles + pills reflect the axis, not a fixed period: header → 일간/주간/월간 브리핑; pills → 일 · 주 · 월. temporalNav (오늘/이번 주/이번 달) stays put — it now feeds ONLY the anchored "on now" marker. i18n keys renamed (homeTitle.day/week/month) + added (homePeriodNav.tabDay/tabWeek/tabMonth, toggleWeekPicker); all code refs fixed.
  • Classification: OTA-safe JS/TS — packages/app (PeriodNavPanel + i18n) + packages/features/home (feed screen, week view, both pickers). No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: fe1bcfa4. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 7b7ab6e6-a340-4760-9a44-ad9fc63b1053 (Android 019f275b-3b03-7c4d-b98e-081ab84b8d93, iOS 019f275b-3b03-7b3e-a944-9b9e157e0c65), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + @twomore/app tests (141 suites / 2,283).
  • Rollback pointer: republish 6ff5231a-6e76-48e9-a4ab-a95c6b57a638. Source: revert fe1bcfa4.

2026-07-03 Preview OTA 6ff5231a — navigable home period axis (store + PeriodNavPanel, week + month)

  • Change: Phase 2 of the home period-nav epic. 오늘/이번 주/이번 달 become a navigation axis — the 홈 pill picks the GRANULARITY, and within it the user scrubs an anchor date. Foundation in packages/app: useHomePeriodStore (MMKV persist; {granularity, anchor, scrubberOpen} + setGranularity (resets anchor to today — the pill IS return-to-now, so no reset chip), step, setAnchor, toggleScrubber; reuses the canonical DateSelection/deriveDateRange; selectors homePeriodRange + isHomePeriodNow), PeriodNavPanel (one-row nav + inline anchored marker; center taps to open a scrubber only when provided — no chevron/reset), the addMonths util, and homePeriodLabel/homePeriodMarkerLabel. Wiring in features/home: the pill drives the store granularity; the week view derives its range from the anchor (arrows step weeks); the month view derives its calendar month (recap switched from rolling-28 to the calendar month) and opens a MonthYearPicker (year stepper + 12-month grid) to free-jump any month. HomeTodayView untouched — per-day nav + a server-side date-range match-history query are Phase 2c (the client-filtered history under-counts the deep past; TODO noted in both views).
  • Classification: OTA-safe JS/TS — packages/app (store + component + util + helpers + i18n + barrel) + packages/features/home (screen + 2 views + MonthYearPicker). No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: 6281de71. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 6ff5231a-6e76-48e9-a4ab-a95c6b57a638 (Android 019f272a-8636-7917-a6b9-1979d2c44539, iOS 019f272a-8636-7c40-9e5b-2253174a646e), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + @twomore/app tests (141 suites / 2,283, incl. the new home-period store test). Not yet device-QA'd (tap-to-open, month picker, anchored-marker states).
  • Rollback pointer: republish cb5c3cf0-430d-43d5-82e6-61e06d3702fc. Source: revert 6281de71.

2026-07-03 Preview OTA cb5c3cf0 — canonical RecapOutcomePanel + full-width breakdown stat strip

  • Change: Phase 1 of the home period-nav epic. Extracted the duplicated 결과 donut block into a shared canonical RecapOutcomePanel (packages/app/src/presentation/components/recap-outcome-panel.tsx) and fixed the left-skewed breakdown. The panel = donut (centered count + label) + hero (win-rate) on one row, a Divider, then a full-width breakdown STRIP of equal-flex cells (always fills width — no more short 2-item legend squeezed left). WeekRecapCard (filled + ghost) now renders it with strip = 승 · 패 · (무 when >0) · ELO(eloDelta) · 연승(currentStreak) — real PeriodRecap data that doesn't duplicate the donut count or win-rate hero. record-detailOutcomeSummaryCard adopts it while PRESERVING its richer SummaryChip grid below (DaySummaryCard left for a follow-up). Kills the duplicated donut layout across home + records. Also codified a "Wireframe-first (UI/UX)" operating principle in CLAUDE.md.
  • Classification: OTA-safe JS/TS — packages/app (new component + i18n + barrels) + packages/features/{home,records}. Plus a docs-only CLAUDE.md change. No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: 0ec8a422 (recap strip 9b4c950b + workflow doc 0ec8a422). App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group cb5c3cf0-430d-43d5-82e6-61e06d3702fc (Android 019f26fa-a8d6-7c28-80b7-4eacb04dc039, iOS 019f26fa-a8d6-72ec-9c50-9fdeec771d95), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + @twomore/app tests (140 suites / 2,272).
  • Rollback pointer: republish 84653b70-29d5-46e2-b481-7225e970d5cf. Source: revert 9b4c950b.

2026-07-03 Preview OTA 84653b70 — canonical EmptyState in match-detail well + recap 결과 donut alignment

  • Change: two focal-surface polish fixes. (1) SessionMatchDetail (the expanded participation-row well) empty state: two loose left-aligned cardMeta lines → the canonical compact EmptyState (Swords icon + title + subtitle, centred), so the empty match well reads as an intentional state on the shared empty-surface design language. (2) WeekRecapCard 결과 outcome panel: the donut's centred count and the win-rate number never shared a line — the legend beside the win-rate block pushed it ~12px above the donut centre. Pulled the legend out to its own full-width row so the right column mirrors the donut-centre block (displayMd number + label); the two hero numbers now align and the legend gets room. record-detail-screen shares this donut pattern (duplicated, not yet a shared component) — flagged for a follow-up unify.
  • Classification: OTA-safe JS/TS — packages/app (session-match-detail) + packages/features/home (week-recap-card). No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: ec839033. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 84653b70-29d5-46e2-b481-7225e970d5cf (Android 019f26c3-5298-7330-94d4-f79e3f08dfc6, iOS 019f26c3-5298-734c-b1f4-33cb726fa286), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + @twomore/app tests (140 suites / 2,272).
  • Rollback pointer: republish 4067f960-c09f-47ff-a4d1-e36b7ef401fa. Source: revert ec839033.

2026-07-03 Preview OTA 4067f960 — revert card focal strips to neutral (court tile carries colour)

  • Change: design walk-back of the session-card focal-strip colour (follows bf4f1a6a / a780927c). With the VenueHero WHERE bar now leading with an informative CourtLinesTile (surface hue), the green-washed strips + green text were a second, competing colour source. Reverted both focal strips to their neutral canonical tokens so the court tile is the single accent: WHEN strip $surfaceTertiary$surfaceSecondary + $primary text → $text; WHERE bar $surfaceTertiary$background + $primary name → $text (court info stays $textSecondary; the CourtLinesTile / MapPin lead stays). The VenueLink pill keeps its earlier ~20% tint bump (separate default-layout element that addressed the original faint-veil note) — flagged for a later call.
  • Classification: OTA-safe JS/TS — packages/app (session-header + venue-hero). No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: 5d676649. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 4067f960-c09f-47ff-a4d1-e36b7ef401fa (Android 019f265a-37a2-7912-9d7d-e653cf38e8b0, iOS 019f265a-37a2-754c-b40d-3e88a882aa65), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + @twomore/app tests (140 suites / 2,272).
  • Rollback pointer: republish bf4f1a6a-0bec-47fe-b9e0-80ee66f00227. Source: revert 5d676649.

2026-07-03 Preview OTA bf4f1a6a — unify the venue-hero WHERE bar onto the WHEN strip's green

  • Change: the session summary card's two focal strips read as two different colours — the session-header WHEN strip is green ($surfaceTertiary wash + $primary text) but the VenueHero WHERE bar was a neutral white/dark strip ($background + $text), noisy side by side. Brought the WHERE bar onto the same green treatment: $background$surfaceTertiary + venue name $text$primary. Court info keeps its $textSecondary hierarchy; the CourtLinesTile keeps its surface hue (the one meaningful accent). Where + when now read as one brand pair; a comment ties the two strips together to keep them in sync.
  • Classification: OTA-safe JS/TS — packages/app (venue-hero only). No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: 35d4d88d. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group bf4f1a6a-0bec-47fe-b9e0-80ee66f00227 (Android 019f2646-5d38-78e4-998a-ddbcf977b54c, iOS 019f2646-5d38-76d9-b32c-3f6b64d98c3d), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + @twomore/app tests (140 suites / 2,272).
  • Rollback pointer: republish 15ca8117-76a5-48e4-8320-fcfad85ae575. Source: revert 35d4d88d.

2026-07-03 Preview OTA 15ca8117 — court-surface mark on the venue hero + drop time-strip clock

  • Change: review refinement of the session-card focal strips (follows a780927c). (1) Dropped the leading $primary Clock icon from the session-header WHEN strip — the $surfaceTertiary wash + $primary date/time text already carry the brand-green cue, so the icon was redundant. (2) Added the court-type primitive to the VenueHero WHERE bar (the "venuerow"): a surface-tinted CourtLinesTile for a known court surface, else the brand MapPin, at a 20px size matched to the VenueLink pill's inline tile. The bar previously led with text only (venue name + court info); it now leads with the court/brand colour cue, so the map hero's bottom bar echoes the VenueLink pill.
  • Classification: OTA-safe JS/TS — packages/app (session-header + venue-hero). No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: 26d94a1d. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 15ca8117-76a5-48e4-8320-fcfad85ae575 (Android 019f261b-cf52-7066-bd53-336a9a409220, iOS 019f261b-cf52-7f55-b2a8-4b9f5aa704ae), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn typecheck + yarn lint:strict (0/0) + @twomore/app tests (140 suites / 2,272). Note: the venue hero renders only in the dev-gated location-first / venueCardMode layout; on-device look at the CourtLinesTile at 20px in the compact bottom bar is the remaining device-QA check.
  • Rollback pointer: republish a780927c-144d-400e-9202-0c439357b023. Source: revert 26d94a1d.

2026-07-03 Preview OTA a780927c — warmer light-mode focal strips on the session card

  • Change: subtle brand-green colour on the session summary card's two focal strips, which read as near-colourless veils in light mode. (1) The session-header WHEN strip (whenBand, location-first layout) moves from $surfaceSecondary (#F6F8F5, near-white) to $surfaceTertiary (#E6F4EC brand wash) and gains a leading $primary Clock icon + $primary date/time text — so the "when" strip mirrors the VenueLink "where" strip's green treatment. (2) VenueLink pill tint alpha bumps ~14% → ~20% (PILL_BG_ALPHA 24 → 33) and the non-court MapPin fallback now tints with the brand green at that same alpha (resolved from $primary) instead of the near-flat $primarySubtle veil.
  • Classification: OTA-safe JS/TS — packages/app (session-header + venue-link). No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: 1a5b807d. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group a780927c-144d-400e-9202-0c439357b023 (Android 019f25e8-dcd2-7521-aa67-ac940565333d, iOS 019f25e8-dcd2-76b0-affa-4a677b7db39c), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn check (52 suites) + yarn lint:strict (0/0). Note: the VenueLink tint bump colours EVERY venue pill app-wide (cards, detail, compact rows), not just the session card — intended as the "more colour" nudge. On-screen saturation on real light-mode devices is the remaining device-QA check; a bolder pass (stronger tint + coloured strip fill) is the held follow-up if this reads too subtle.
  • Rollback pointer: republish 7f0b23b5-6670-43cd-b62f-41d71a33c123. Source: revert 1a5b807d.

2026-07-03 Preview OTA 7f0b23b5 — match-type chip + session-detail 경기 uses the scorecard rows

  • Change: (1) new canonical matchFormatLabel(format) helper (short-form 단식/복식/혼복 — replaces several ad-hoc inline copies). (2) MatchRowHeader (the shared header for every scorecard row + now the session-detail 경기) renders a match-type chip from the stored per-match match.format — no gender needed — so a mixed-format session's singles + doubles matches are told apart on the inline ELO-bar cards. (3) SessionMatchupsCard (session-detail 경기) now uses the SAME components as the 대진표/scorecard rows: MatchRowHeader (코트·라운드 + status/type chips) above MatchScoreboard, with status-aware scores (completed/live show real games + winner; scheduled show the ELO tug-of-war bar). Dropped the separate muted ProgressBar — a redundant second balance bar now that MatchScoreboard's VsDivider renders the "N% 우세" tug-of-war bar.
  • Classification: OTA-safe JS/TS — packages/app (helper + MatchRowHeader) + packages/features/sessions (SessionMatchupsCard). No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: 1e0c5cca. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 7f0b23b5-6670-43cd-b62f-41d71a33c123 (Android 019f25a4-9e51-76ad-bddf-af6147c40476, iOS 019f25a4-9e51-7e21-ba1b-936af0faa195), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn check + yarn lint:strict (0/0). Note: the format chip now renders on ALL MatchRowHeader surfaces (scorecard draws/completed/live + the session-detail 경기 + any home live-card rows) — intended for consistency; an on-device look at chip density on a pure-format session is a nice follow-up.
  • Rollback pointer: republish 229d18e9-f700-4744-992e-788c002ef60a. Source: revert 1e0c5cca.

2026-07-03 Preview OTA 229d18e9 — host-only gated 일정 삭제 (cancel-first + refund gate)

  • Change: reintroduces session deletion as a HOST-only danger zone at the bottom of the session-detail scroll (SectionBlock tone="danger" + destructive ActionButton + useConfirm — the canonical danger-zone pattern). On the detail rather than the 일정 관리 hub because the hub is unreachable once a session is cancelled (its sections are status-gated) — which is exactly when the delete is needed — and the detail already carries the host state (RSVPs, refund status) + the host's other actions. Gate: solo session → delete directly; has other participants + not cancelled → tap prompts "cancel first" (cancellation notifies + refunds; delete is the final purge); cancelled + refunds pending → "finish refunds first" (the 00307 BEFORE-DELETE trigger backstops it); cancelled
    • settled → confirm → delete. Hidden on in_progress / completed (match records + ELO history). New i18n deleteBlockedCancelFirst.
  • Classification: OTA-safe JS/TS — packages/features/sessions (detail) + i18n. No migration, no native change (nativeSensitivePaths diff since 255bdfa0 empty).
  • Source commit: b6d5b2d7. App / runtime version: 0.6.2 (JS-only).
  • OTA artifact: preview update group 229d18e9-f700-4744-992e-788c002ef60a (Android 019f2576-7055-7327-9268-0e693e9d8a7f, iOS 019f2576-7055-7328-94e8-2206c5602b0a), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence pinned to build 255bdfa0.
  • Verification: yarn check + yarn lint:strict (0/0). On-device QA of the gated-delete branches (cancel-first prompt / refund gate / actual delete) is still pending.
  • Rollback pointer: republish 0c2be8b0-eabb-442d-b204-75b2dd92a890. Source: revert b6d5b2d7.

2026-07-03 Preview OTA 0c2be8b0 — canonical config pickers + match dividers + configurable match rules

  • Change: five session-management UX fixes. (1) New Stepper primitive (packages/ui) — the canonical ± number control (none existed). (2) Session-detail 경기 card gets the canonical inter-match Divider inset="full" (between consecutive matches only), matching the 경기 page. (3) 일정 관리 config form: dropped the page-title subtext; swapped raw AdminField text inputs for the create wizard's controls — native date/time pickers (DateTimePickerModal + PickerRow), StyledInput (venue/description), and the new Stepper (max players, now numeric state). (4) 일정 삭제 removed from 일정 관리 (+ its 위험 구역): cancelling is the host's 일정 취소 on the detail page, not a hub item for every admin. (5) 참가자 관리: the 제거 CTA moved INSIDE the participant Card. (6) 경기 규칙: 시작 점수 / 한 세트 게임 수 / 차이로 승리 are now Steppers, not fixed chips — domain schema widened (winBy 1|2 literal → int 1-3; startingScore '0-0'|'1-1' enum → symmetric "N-N" regex string). The rule values are advisory/display only (server writes p_rules verbatim; completion is by consensus), so no scoring-engine impact; round-rules-strip already typed winBy: number.
  • Classification: OTA-safe JS/TS — packages/ui (Stepper) + packages/features/sessions + packages/app (match-rules entity + i18n). No migration, no new native dep (react-native-modal-datetime-picker was already bundled for create-session). git diff b28a60ad..HEAD over nativeSensitivePaths is empty.
  • Source commit: 9ae26bad.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 0c2be8b0-eabb-442d-b204-75b2dd92a890 (Android 019f2476-efb9-7347-bbd3-9572e42dfce3, iOS 019f2476-efb9-7c65-80e0-804f14ba77c2), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0.
  • Verification: yarn check (typecheck + full test suite incl. the widened match-rules entity tests) + yarn lint:strict (0/0). On-device QA of the native date/time pickers + the steppers is still pending (layout/interaction change, not covered by unit tests).
  • Rollback pointer: republish 0fdff3d9-5e36-4218-b61a-4965c093e7a7. Source: revert 9ae26bad.

2026-07-03 Preview OTA 0fdff3d9 — canonical HeaderIconButton (fix chunky share/message header icons)

  • Change: the session-detail share + message header icons used Button variant="ghost" size="sm" (16px text-button padding + a 36px frame), so they read chunky, over-spaced and misaligned next to the tight Pressable variant="icon" back arrow. New HeaderIconButton primitive (packages/ui) gives every header icon one treatment — a tight 24px (xl) icon with a ~44pt hitSlop touch target and NO layout padding, so icons stay flush + edge-aligned and space purely by their container gap. AppHeader's back button now uses it too (single source of truth); the session-detail header gap tightened to $3. The gear overflow (HeaderActionsMenu) stays a Popover.Trigger Button (it only co-appears on live sessions; converting a shared popover trigger wants device QA).
  • Classification: OTA-safe JS/TS — packages/ui (new primitive + app-header) + packages/features/sessions (detail header). No migration, no native change (git diff b28a60ad..HEAD over nativeSensitivePaths empty).
  • Source commit: 91a5445b.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 0fdff3d9-5e36-4218-b61a-4965c093e7a7 (Android 019f2461-061c-7218-ac86-76e1f9436759, iOS 019f2461-061c-7f38-a845-2080ed7d1ab5), branch preview. Baseline advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0.
  • Verification: yarn check + yarn lint:strict, all green. On-device visual confirmation of the tightened spacing is still pending (pure layout change).
  • Rollback pointer: republish 3b080729-9e11-4bc7-a3e8-18d68dcd7d2a. Source: revert 91a5445b.

2026-07-02 Backend deploy 00308 + Preview OTA 3b080729 — completed-match score correction + host refund-obligation surface

  • Change: two owner-approved gap closures. B2 — completed-match score correction (00308): match_score_calls += call_type ('confirm'|'correct_score'); propose_score_correction (host/admin, completed-only) opens a correction call auto-agreed by the proposer; agree_to_correction accumulates participant consent and, on the last agreement, writes the corrected score (SECDEF bypasses the protect_matches_scoring trigger) + re-derives ALL ratings via the proven private.recompute_all_ratings() — the only safe re-application (apply_match_ratings no-ops on an already-rated match; a manual reversal would double-count player_matchups/club_elo_ratings). reject_call cancels either type. Client: callType on entity/adapter/port; useProposeScoreCorrection + useAgreeToCorrection hooks; ScoreCorrectionSheet (propose steppers + consent rows) wired to the completed-match 점수 입력 action. Host refund obligation: the host's cancelled-paid session detail shows a 환불 미정산 banner (amber within the 72h grace, red overdue) tapping to the 정산 checklist — closes the 00307 deferred visible-obligation gap.
  • Classification: backend deploy + OTA-safe JS/TS. DB migration 00308 pushed to the linked hosted project (llhvhhpbjnnrfwcczizc; migration list confirmed an only-00308 delta). No edge-function change (B2 reuses the existing 00240 score-call signal). Client is packages/app + packages/features/sessions; no native-sensitive file changed since the 0.6.2 baseline (git diff b28a60ad..HEAD over nativeSensitivePaths is empty).
  • Source commit: b6047bd93d17b006520aba35f7e053a0165d7832.
  • Source sync: pushed to origin/main; pre-push gates all green.
  • App / runtime version: 0.6.2 (unchanged — JS-only client).
  • OTA artifact: preview update group 3b080729-9e11-4bc7-a3e8-18d68dcd7d2a (Android 019f20b3-8094-775b-b256-3e813aa51eaa, iOS 019f20b3-8094-7d46-96c0-d00075cadbc0), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: npx supabase db reset (full chain applies clean) + yarn check (typecheck + docs + full test suite) + yarn lint:strict (0/0). Runtime probe (docker psql, txn ROLLBACK): a 6-0 → 0-6 correction flipped ratings symmetrically (p1=1030>p2=970 became p2=1030>p1=970); the correction call cleared; a non-admin was blocked from proposing. On-device QA of the live consent UI + the recompute latency is still pending (proven by DB probe + tests, not a device run). Note: recompute_all_ratings is a GLOBAL rebuild (heavy but correct) — acceptable for a rare admin correction.
  • Rollback pointer: republish the prior compatible group 0611c21d-895a-4866-86ca-3203b2f3e003, or eas update:roll-back-to-embedded --branch preview. Source: revert b6047bd9. DB: 00308 is additive (a column + two RPCs) — safe to leave; a down-migration would DROP them.

2026-07-02 Backend deploy 00307 + Preview OTA 0611c21d — session refund-acknowledgment + host accountability

  • Change: closes the documented gap (since 00299) where cancelling a PAID session left paid rows untouched and a hard DELETE CASCADE-wiped the payment trail. Schema (00307): session_payments += refund_state (none|pending|acknowledged) + refund_pending_at + refund_acked_at; new host_reliability_events table (durable accountability ding). Flow: a trigger on session→cancelled opens a refund obligation on each paid row (excluding the host) + emits a session_refund_notice signal; the unified cancel_session_as_host RPC soft-cancels pickup AND club (club no longer hard-deletes on cancel); acknowledge_session_refund (환불 받았어요) settles a row and, on the last one, resolves the host ding + obligation signal; an hourly cron dings hosts unsettled past a 72h grace window + escalates a persistent host_refund_unsettled signal; a BEFORE DELETE trigger blocks removal while refunds pend. Client/UI: refund fields on the SessionPayment entity/mapper/adapter/port + 2 hooks; 2 new SignalTypes + ko/en signal copy + send-push titles; 일정 취소 → unified soft cancel w/ refund-aware confirm copy; cancelled-paid payer sees a 환불 받았어요 CTA + notice; 정산 → refund checklist (환불 대기/확인됨 per row); 위험 구역 delete gated while refunds pend.
  • Classification: backend deploy + OTA-safe JS/TS. DB migration 00307 pushed to the linked hosted project (llhvhhpbjnnrfwcczizc); the send-push edge function redeployed (2 new refund push titles). Client is packages/app + packages/features/sessions + i18n; no native-sensitive file changed since the 0.6.2 baseline (git diff b28a60ad..HEAD over nativeSensitivePaths is empty).
  • Source commit: 6702f2ebe2a0bbcc10cefa2d4b5a7cf3081262df.
  • Source sync: pushed to origin/main; pre-push gates all green.
  • App / runtime version: 0.6.2 (unchanged — JS-only client).
  • Hosted DB: npx supabase db push --linked applied ONLY 00307 (remote was at 00306); npx supabase migration list delta confirmed pre-push. Edge fn: npx supabase functions deploy send-push --no-verify-jwt (exit 0).
  • OTA artifact: preview update group 0611c21d-895a-4866-86ca-3203b2f3e003 (Android 019f208a-6c71-7a39-9dda-7c5788fbf5f2, iOS 019f208a-6c71-75b0-8775-0c8d7f316eb4), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: npx supabase db reset (full migration chain applies clean) + yarn check (typecheck + docs + full test suite, incl. i18n terminology) + yarn lint:strict (0/0). NOTE: on-device QA of the live refund round-trip (cancel → signal push → 환불 받았어요 ack → host ding after grace) is not yet done — the flow is proven by local db-reset + tests, not a device run.
  • Rollback pointer: republish the prior compatible group d235127b-cd5b-4e5c-879a-cbdbeba00042, or eas update:roll-back-to-embedded --branch preview. Source: revert 6702f2eb. DB: 00307 is additive (columns default 'none', new table/trigger/cron/RPCs) — safe to leave; a down-migration would DROP the new objects.

2026-07-02 Preview OTA d235127b — session-detail CTA/header rework + canonical HostBadge

  • Change: (1) host/admin session-detail bottom band is now a two-button row [일정 관리][contextual] (ActionButton secondary + danger, flex 1:1 — the canonical two-button BottomCtaBand pattern); the edit affordance moved off the header Pencil into the 일정 관리 CTA; contextual = 일정 취소 (open|locked), 스코어카드 (in_progress), 매너 태그 (completed), 관리-alone when no contextual action. (2) The session group chat moved from the gear kebab to a header MessageCircle icon; the kebab now holds only live utilities (GPS/기권) and auto-hides on non-live sessions. (3) Header controls unified to one canonical treatment (Button variant="ghost" size="sm" + Icon size="xl" + $text) so Share/Message/gear match in size, spacing, color. (4) New canonical HostBadge (crown + brand-tint accent; flips to warning "승인 N" when applications pend) replaces the loud $primary host strip on both the card (title slot) and detail (status-banner trailing slot — one strip, no second bar); removed the now-dead SessionTopBanner host tone.
  • Known follow-up: the message icon opens the create-group flow (no persistent per-session chat room yet — dm_threads has no session_id); a real one-room-per-session needs a migration (tracked, not in this OTA).
  • Classification: OTA-safe JS/TS — packages/app (presentation: session-card, session-top-banner, new host-badge; i18n) + packages/features/sessions (detail screen); no native-sensitive file changed since the 0.6.2 baseline (git diff b28a60ad..HEAD over nativeSensitivePaths is empty).
  • Source commit: 672db074a647d18fdc61d1528c99375d5c33e770.
  • Source sync: pushed to origin/main; pre-push gates all green.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group d235127b-cd5b-4e5c-879a-cbdbeba00042 (Android 019f1eb7-eca4-7595-9e2b-e66c8272e05b, iOS 019f1eb7-eca4-726b-b05a-6bc8cce73b7a), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn typecheck + yarn test (exit 0) + yarn lint:strict (0/0); pre-push gates all green.
  • Rollback pointer: republish the prior compatible group d83768f4-ddf7-4972-8a67-e5f5ae7c7bab, or eas update:roll-back-to-embedded --branch preview. Source: revert 672db074.

2026-07-01 Preview OTA d83768f4 — edit → 일정 관리 hub + shrunk detail kebab

  • Change: EditSessionScreen is now a 일정 관리 management hub (DetailShell + SectionBlocks, mirroring ClubSettingsScreen) with sections rendered by status: 설정 (config form, open) · 참가·모집 (참가자/규칙/호스트 관리, open|locked) · 경기 운영 (경기 보드/출석/경기 종료, in_progress|completed) · 정산 (in_progress|completed) · ⚠︎ 위험 구역 (일정 삭제). Each row reuses the exact route/mutation + gate the kebab used (relocation, not a permission change). The Pencil header icon now opens it for canManageSession at any status. The detail kebab dropped the 8 host-management items (now in the hub), keeping only 3 participant utilities (그룹 채팅/GPS/기권); the gear auto-hides when none apply. Host banner + share/edit icons + role-based bottom CTA unchanged. (Fixed an i18n terminology-test miss: hub label 세션 관리 → 일정 관리 [ko] / Event Management [en], End Session → End event.)
  • Classification: OTA-safe JS/TS — packages/features/sessions (edit + detail screens) + i18n; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: 3c697da373d2206e9af235cf92f9ff22b1bff464.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group d83768f4-ddf7-4972-8a67-e5f5ae7c7bab (Android 019f1db7-1d3c-70ee-9655-b7557460f90f, iOS 019f1db7-1d3c-7914-a0e2-0cc3d7379a84), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (2,796 tests incl. the i18n terminology test) + yarn lint:strict (0/0); pre-push gates all green.
  • Rollback pointer: republish the prior compatible group 80ffe522-33d3-4943-ad45-01f64f62eac3, or eas update:roll-back-to-embedded --branch preview. Source: revert 3c697da3.

2026-07-01 Preview OTA 80ffe522 — stronger host indicator (내가 여는 일정 top banner)

  • Change: the weak 호스트 metadata chip is replaced by the app's canonical strong host signal — SessionTopBanner host tone (full-bleed $primary top strip), now firing for any session the viewer hosts (subordinate to live/cancelled). Label "내가 여는 일정", or "승인 대기 N건" when a pickup host has pending apps. The leading 호스트 chip removed from the header chip strip. The session-detail page gets a matching $primary host-identity strip below the status banner. Priority: live > cancelled > host-identity > deadline/almostFull; non-hosts unchanged.
  • Classification: OTA-safe JS/TS — packages/app (session-card/header + i18n) + packages/features/sessions (session-detail-screen); no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: 23820d6fddc589a772d064ad6e36b08bf089c79b.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 80ffe522-33d3-4943-ad45-01f64f62eac3 (Android 019f1d93-a315-73b0-a485-b0828dcb804c, iOS 019f1d93-a315-775c-81dc-dc2fb91c4d0b), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (all workspaces green) + yarn lint:strict (0/0).
  • Rollback pointer: republish the prior compatible group 004516e0-779d-45c7-858d-70a2eb209cff, or eas update:roll-back-to-embedded --branch preview. Source: revert 23820d6f.

2026-07-01 Preview OTA 004516e0 — role-based session-detail CTA + share/edit header icons

  • Change: session-detail actions are role-based now. The bottom CTA checks host first: a host (createdBy===userId) sees 일정 취소 (destructive) on open/locked — never 참가 취소; non-host confirmed participants keep 참가 취소; every other state (join / guest / tier-blocked / in_progress→scorecard / completed→manner-tag) is unchanged. 일정 취소 wires to cancel_pickup_session_without_strike for pickups (publicInviteEnabled) or a club hard-delete; when the fee-gated session has confirmed participants, the confirm dialog warns that refunds are the host's responsibility (interim — the consent-gated refund flow is the next slice). Share + edit are now quick header icons (edit host/admin + open only) removed from the kebab; the fuller edit-as-management-hub is a follow-up.
  • Classification: OTA-safe JS/TS — packages/features/sessions/session-detail-screen.tsx
    • i18n; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: 5ea159c24d8e321298f0fdc9dfeac138c24095a1.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 004516e0-779d-45c7-858d-70a2eb209cff (Android 019f1d57-ae15-76d7-b087-78f4dfd5a09b, iOS 019f1d57-ae15-70c5-845f-6256a4c9dc7e), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (all workspaces green) + yarn lint:strict (0/0).
  • Rollback pointer: republish the prior compatible group c7eedd6b-5daa-4e46-9460-c5be4faf7bba, or eas update:roll-back-to-embedded --branch preview. Source: revert 5ea159c2.

2026-07-01 Preview OTA c7eedd6b — host badge + host-first ordering + hide own from discovery

  • Change: (1) 호스트 badge — SessionCard forwards its isHost (createdBy===userId) to SessionHeader, which renders a leading accent 호스트 chip in both the chip strip + the two-tier Row 1. (2) Discovery exclusion — findOpenPickupsPage gained an excludeCreatedBy option (server .neq, pagination-safe) threaded from the pickup-finder + pickups screens as the viewer's userId (+ query-key update), so own hosted pickups no longer show in 번개 찾기. (3) Host-first ordering — new sortSessionsHostFirst (date primary → host-first within day → time) applied to useMyParticipatingSessions + the 오늘/이번 주/이번 달 views; server ORDER BY untouched.
  • Classification: OTA-safe JS/TS — packages/app + packages/features/{home,activity}; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: c4d5701033415201476ea6bd854124dfd5939289.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group c7eedd6b-5daa-4e46-9460-c5be4faf7bba (Android 019f1d31-bae0-707a-b5a0-8a0e552b7823, iOS 019f1d31-bae0-7d85-add1-38bc198726cf), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (all workspaces green) + yarn lint:strict (0/0).
  • Rollback pointer: republish the prior compatible group b60a7171-8477-496d-a2da-caa8548087e2, or eas update:roll-back-to-embedded --branch preview. Source: revert c4d57010.

2026-07-01 Preview OTA b60a7171 — match-popup action suite (선수 교체 + 점수 입력)

  • Change: the match popup (MatchProfilesSheet) now carries a permanent action suite. MatchActionSuite renders 선수 교체 (lineup edit / substitution) + 점수 입력 (score entry); a button is enabled iff its callback is provided, else shown greyed with a reason. Enable matrix: 선수 교체 → scheduled (admin); 점수 입력 → live (confirmed RSVP); both disabled on completed (점수 입력 reason "전원 동의 필요" — the consent flow is the next slice). Wired across all 3 tabs: 일정 mounts a SwapSheet (allMatches + confirmedIds threaded from the scorecard) opened via 선수 교체; LIVE opens the existing ScoreEditSheet via 점수 입력; 결과 shows both disabled. Existing card-tap flows + the draws 경기 규칙 button unchanged.
  • Classification: OTA-safe JS/TS — packages/app i18n + packages/features/sessions/*; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: e85bcf13681883e7b74519e72b222aa9fe366ebe.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group b60a7171-8477-496d-a2da-caa8548087e2 (Android 019f1b18-a0a3-77d9-9559-7db8ac48a61b, iOS 019f1b18-a0a3-78d1-86ba-feebe3ac171c), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (2,796 tests) + yarn lint:strict (0/0).
  • Rollback pointer: republish the prior compatible group 1ecbd7ff-f4f1-4160-8730-b672b2daf1f1, or eas update:roll-back-to-embedded --branch preview. Source: revert e85bcf13.

2026-07-01 Preview OTA 1ecbd7ff — mutually-exclusive scorecard tabs (일정/LIVE/결과)

  • Change: session surfaces made mutually exclusive by match status + two tab renames. 대진표 → 일정 (Schedule) now shows ONLY scheduled matches (dropped the in_progress matches + the 완료된 경기 completed section); 라운드 결과 → 결과 (Results) keeps completed; LIVE keeps in_progress — so a match flows 일정 → LIVE → 결과 with no overlap. Also reverted the earlier week/month live-session compose-in: 이번 주 / 이번 달 exclude live now (live lives only in 오늘, which still shows ALL concurrent live sessions).
  • Classification: OTA-safe JS/TS — packages/app i18n + packages/features/{sessions,home}; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: d3aa96a11b37b305291b542162970bb078b54012.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 1ecbd7ff-f4f1-4160-8730-b672b2daf1f1 (Android 019f1afb-c02b-764d-90af-af980cb4b79b, iOS 019f1afb-c02b-7fc6-bc59-93655753a57d), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced; lastCompatibleSourceCommit + evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (all workspaces green) + yarn lint:strict (0/0).
  • Rollback pointer: republish the prior compatible group d6812462-e0fa-4378-848e-49af693561ec, or eas update:roll-back-to-embedded --branch preview. Source: revert d3aa96a1.

2026-07-01 Preview OTA d6812462 — concurrent live sessions + defer text cards + match-swap impact editor

  • Change: three owner items, two commits (f079b2c8 fixes, 3a1ace93 editor). (1) 오늘/이번 주/이번 달 only showed ONE live session — home-today-view + live-session-stack used .find(in_progress) → switched to .filter() + render all; week/month composed in-range live sessions from the cached useActiveAndRecentSessions (didn't mutate the shared findUpcoming, which also gates a pickup rec). (2) Deferred the text-only cards (내 다음 경기 banner + live-count summary on the live tab, totals summary on 라운드 결과). (3) Match-swap impact editor: new pure analyzeSubstitution domain fn (11 tests) reports the matches-played delta + same-round conflict + unconfirmed flag + repeat partner/opponent a swap would introduce; wired into SwapSheet as an inline impact preview (advisory); swap gated to scheduled matches only (in_progress + completed locked) in match-card + the live-round editor.
  • Classification: OTA-safe JS/TS — packages/app (domain rule + i18n + exports), packages/features/home + packages/features/sessions; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commits: f079b2c8b... (live sessions + card defer) and 3a1ace93a09a08f7e84ec03ed646500ace7bd231 (match-swap editor).
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group d6812462-e0fa-4378-848e-49af693561ec (Android 019f19f8-fba2-7e51-a934-7391592d9d14, iOS 019f19f8-fba2-7451-97d5-c5129f929429), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group; lastCompatibleSourceCommit
    • evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (2,796 tests incl. 11 new analyzeSubstitution tests) + yarn lint:strict (0/0), all green.
  • Rollback pointer: republish the prior compatible group 364a3191-699b-4aac-9d67-1769a4778de8, or eas update:roll-back-to-embedded --branch preview. Source: revert 3a1ace93 then f079b2c8.

2026-07-01 Preview OTA 364a3191 — sticky sections on live + rounds tabs + reworked 내 다음 경기

  • Change: ship 2 of the scorecard rework (ship 1's sticky foundation device- validated by the owner — iOS + Android sticky confirmed). (1) Header spacing: round sections use a renderSectionFooter ($3) instead of a separator, so collapsed round headers AND the 완료된 경기 group get the canonical gap (separators skipped empty sections). (2) LIVE tab → RoundSectionList for structured sessions (sticky round header + grouped live-match bodies); ad-hoc stays standalone cards. LiveMatchRow gained variant='card'|'embedded' — embedded keeps the per-match 경기 종료 footer + score/call/profile sheets inside the section body. (3) 내 다음 경기 card reworked: leads with the wait estimate (약 N분 후 · M라운드 대기 — client-side from rounds-to-wait × rotation/median, no query), keeps current-round progress, adds a 곧 시작·준비하세요 imminent nudge, and the upcoming match's ELO stakes; useViewerRoundContext now exposes currentRound. (4) 라운드 결과 tab → RoundSectionList (most-recent round open); CompletedRow gained variant='card'|'embedded' (winner highlight + ELO delta kept); totals summary in the list header.
  • Classification: OTA-safe JS/TS — packages/app (use-viewer-round-context + i18n) + packages/features/sessions/src/scorecard/* + spectator-scorecard-screen; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: eefeb1d1344ac03c72ae4f798522eb5b138f9191.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 364a3191-699b-4aac-9d67-1769a4778de8 (Android 019f19bf-f4af-7395-8a93-d8c9c48692d9, iOS 019f19bf-f4af-7641-bcca-40cad0cd129c), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group; lastCompatibleSourceCommit
    • evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (all workspaces green) + yarn lint:strict (0/0). All 3 scorecard tabs now share the sticky round-section / standalone-card pattern keyed on session structure.
  • Rollback pointer: republish the prior compatible group 6edd5d84-1a3b-4bb2-88de-7f932a1a5ce6, or eas update:roll-back-to-embedded --branch preview. Source: revert eefeb1d1.

2026-07-01 Preview OTA 6edd5d84 — sticky round sections (draws) + match competitiveness colors

  • Change: ship 1 of the scorecard rework (foundation; live + rounds tabs follow after device validation). (1) Sticky round headers — the single-item RoundCard can't have a sticky header, so each round is now a SectionList section (sticky card-top header over a collapsible card-bottom body of embedded match rows). New RoundSectionList (features) + a sanctioned StickySectionList primitive in @twomore/ui (features can't import RN SectionList directly — COMP-5). Draws structured path converted; ad-hoc unchanged; RoundCard deleted. (2) Match competitiveness — teams color-coded (team1 green $primary / team2 amber $accentWarm left-edge bar, pre-result only) so the tug-of-war bar maps; the bar labels the favored side "우세 N%" (favored team's win probability), color-matched.
  • Classification: OTA-safe JS/TS — packages/ui (new primitive + export), packages/app (match-scoreboard + i18n), packages/features/sessions/src/scorecard/*; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: 6530bc1b38fdd259244c9c86a869767505589966.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 6edd5d84-1a3b-4bb2-88de-7f932a1a5ce6 (Android 019f19a2-93bf-75bb-b386-6ccb3b243073, iOS 019f19a2-93bf-70ee-ab88-767a2d44fd97), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group; lastCompatibleSourceCommit
    • evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (all workspaces green) + yarn lint:strict (0/0). ON-DEVICE VALIDATION NEEDED before ship 2: iOS sticky behavior + the card-top↔ card-bottom seam (no pixel gap), and Android SectionList stickiness (explicit stickySectionHeadersEnabled; verify it sticks, else add an animated fallback).
  • Rollback pointer: republish the prior compatible group 6abefd37-a574-4b34-94d3-ce7969edd482, or eas update:roll-back-to-embedded --branch preview. Source: revert 6530bc1b.

2026-07-01 Preview OTA 6abefd37 — round cards for structured draws + canonical live-card touch

  • Change: owner session-card review. (1) Live cards dropped the home-feed whole-card→scorecard override (live-session-stack.tsx) so they follow the canonical touch model (card surface → session detail; participation row → expand → scorecard) — both destinations reachable. (2) New RoundCard: one collapsible card per round (header + status + rules strip, then the round's match bodies inside the same Card, full-bleed dividers), replacing the inset/differential-spacing grouping. draws-tab now detects structured (matches span >1 round OR session.rotationMinutes set) → RoundCards vs ad-hoc → standalone match cards. DrawsRow gained a variant='card'|'embedded' prop; round header content extracted to RoundHeaderContent (shared by RoundHeaderRow
    • RoundCard). Collapse state + back-to-popup unchanged.
  • Classification: OTA-safe JS/TS — packages/features/home + packages/features/sessions/src/scorecard/*; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: 1554f7b47a0179ff0184da3d6d79a9a058803b73.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 6abefd37-a574-4b34-94d3-ce7969edd482 (Android 019f1975-056f-7006-a8a9-0e8d60d65b49, iOS 019f1975-056f-7f07-95cb-d0c84c382ab1), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group; lastCompatibleSourceCommit
    • evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (all workspaces green) + yarn lint:strict (0/0). The seeded live_showcase sessions set rotationMinutes, so they render as RoundCards on device.
  • Rollback pointer: republish the prior compatible group b4acbbf6-1ccd-420d-9973-d25219e7bd16, or eas update:roll-back-to-embedded --branch preview. Source: revert 1554f7b4.

2026-07-01 Preview OTA b4acbbf6 — live_showcase seed scenario + dev-panel live remap

  • Change: QA data was too thin/uniform to eyeball the new session UI (esp. live 대진표 cards). Added a live_showcase seed scenario (seed-scenario edge fn) + widened the generic session generator + remapped the dev-panel "Live" builder edge to it. The OTA itself carries only the JS/TS app-side pieces: dev-panel/types.ts (live edge → live_showcase), scenario-chains.json (live_showcase + qa_showcase chains), and the builder-plan test. The edge function is deployed separately (server-side, not in this OTA).
  • Classification: OTA-safe JS/TS (dev-panel only) + a separately-deployed edge function; no native-sensitive file changed since the 0.6.2 baseline.
  • Backend: supabase functions deploy seed-scenario --no-verify-jwt to project llhvhhpbjnnrfwcczizc (bundle compiled clean). Ran live_showcase against preview: 30 users, 4 clubs, 16 live sessions, 206 matches in 28s.
  • Source commit: 1903a9c4e1e834c8f04851f4bafbbcc41c05e67b.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group b4acbbf6-1ccd-420d-9973-d25219e7bd16 (Android 019f1952-527a-79dc-b46e-21c45aa70b1f, iOS 019f1952-527a-740b-86ec-c515c7ebc5f4), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group; lastCompatibleSourceCommit
    • evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (all workspaces green) + the dev-panel scenario test; DB read-back on preview confirmed 68 sessions (17 live), full court/fee/format/type variety. Edge fn is not in yarn typecheck (Deno) — it was typechecked by the supabase functions deploy bundle step.
  • Rollback pointer: republish the prior compatible group db7b38f9-f58b-49e6-9b13-97f682e75dc2, or eas update:roll-back-to-embedded --branch preview. Source: revert 1903a9c4. The seeded preview data is disposable (any later seed truncates it).

2026-07-01 Preview OTA db7b38f9 — tug-of-war balance + team-grouped popup + referee removal

  • Change: second 대진표/live-session pass on owner feedback. (1) Round header back to a Card; the card-on-card gap fixed by GROUPING instead — matches inset under their header, FeedList separator disabled, differential paddingBottom hugs a round and pushes rounds apart. (2) ELO balance rebuilt as a "tug-of-war" INSIDE the MatchScoreboard VS divider: the two divider bars size to each team's win probability (getExpectedScore over effective team ratings), VS badge slides toward the underdog; neutral fallback for completed/unresolved (replaces the flat ProgressBar, removed from draws-row). (3) Popup: full RoundRulesStrip (5 facets), team-grouped player list with a VS separator (no per-player dividers), expected ELO swing restyled as a graphical chip (▲+win green / ▼lose red). (4) Back-from-profile reopens the popup via useFocusedEffect (newly exported from @twomore/app). (5) Referee UI removed from ALL session surfaces (draws-row, live-row, popup, live-tab 경기 종료 → host/admin); referee-actions-sheet.tsx deleted; backend hooks stay dormant.
  • Classification: OTA-safe JS/TS — packages/app (match-scoreboard, index export)
    • packages/features/sessions/src/scorecard/*; no native-sensitive file changed since the 0.6.2 baseline (git diff b28a60ad..HEAD over nativeSensitivePaths empty).
  • Source commit: 81e0a079c37ffb483132009b10765dc1d8cd8d56.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group db7b38f9-f58b-49e6-9b13-97f682e75dc2 (Android 019f1924-7e44-73c3-b67d-2083af85d478, iOS 019f1924-7e44-7551-8b72-7fb5edd37b54), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group; lastCompatibleSourceCommit
    • evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (typecheck + 2,259 @twomore/app tests, all workspaces green) + yarn lint:strict (0/0). Note: the tug-of-war lives in the shared MatchScoreboard, so live matches also render it (pre-match ELO, not live score) — flagged for on-device QA.
  • Rollback pointer: republish the prior compatible group 2aa80eee-89fa-4908-8551-7855ece362e3, or eas update:roll-back-to-embedded --branch preview. Source: revert 81e0a079.

2026-06-30 Preview OTA 2aa80eee — canonical 대진표 surface + popup + expected ELO

  • Change: live-session 대진표 (draws) sweep brought to the canonical Text/Badge model + new metadata. Round header dropped its Card surface → bare section header (fixes the card-on-card gap) with the summary moved to the title row's top-right; canonical cardTitle/cardMeta roles (no ad-hoc +700 weight or $textTertiary override). Rules strip's hand-rolled RuleChip → Badge neutral sm. Match cards gained an ELO-balance ProgressBar (variant muted) from the already-loaded eloMap (Protocol E — degrades to nothing when ELO is unresolved). The participant popup got a canonical header + a match-detail block (MatchStatusBadge + tabular score, effective-rules Badge chips, referee line) and per-player expected ELO swing (win +X / lose −Y) for scheduled/live matches via the new getExpectedEloOutcomes domain helper (hidden for completed/cancelled and when ratings can't resolve).
  • Classification: OTA-safe JS/TS — packages/app domain rule + i18n + packages/features/sessions/src/scorecard/*; no native-sensitive file changed since the 0.6.2 baseline (verified git diff b28a60ad..HEAD over nativeSensitivePaths is empty).
  • Source commit: 9af4af62b3a2d997f60857cb60f327a85d4b7cf0.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 2aa80eee-89fa-4908-8551-7855ece362e3 (Android 019f18da-2f76-7414-834c-be8572235553, iOS 019f18da-2f76-784b-99b3-7ffa966e554d), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group; lastCompatibleSourceCommit
    • evidence stay pinned to build 255bdfa0 (no native change).
  • Verification: yarn check (typecheck + 2,259 @twomore/app tests, all workspaces green) + yarn lint:strict (0/0) on the 11 touched files; the new getExpectedEloOutcomes carries 3 unit tests (even ±16, favored, underdog).
  • Rollback pointer: republish the prior compatible group 7f6fa03d-6b61-4ebf-b98a-5806c418556d, or eas update:roll-back-to-embedded --branch preview. Source: revert 9af4af62.

2026-06-30 Preview OTA 7f6fa03d — canonical chip flow (type leads, fee always)

  • Change: grounded the location-first chip strip in the SessionInfoModel gate order (eligibility → kind → cost). Type (번개/클럽) is the eligibility frame → ALWAYS leads Row 1; fee is the cost gate → ALWAYS anchors Row 2's right edge (무료 when free). Dropped the dynamic promotion logic (rowEndLabel/rowOneTypeLabel + the feeShown gate that hid fee on member-only-free club and substituted 클럽). Invariant: Row 1 [type][format]…[tier], Row 2 [style][rotation?][approval?]…[fee] — decision gates (tier, fee) at the right edges, explanatory left/middle. One consistent pattern, no special cases. Props renamed back to typeLabel/feeLabel.
  • Classification: OTA-safe JS/TS — session-card.tsx, session-header.tsx; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: 942db9a90d1a95e21afea972f51721b8c98d1283.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 7f6fa03d-6b61-4ebf-b98a-5806c418556d (Android 019f1883-e9d5-7b25-a04e-715ebd45b8fe, iOS 019f1883-e9d5-7ec1-a8a1-ac164e89802e), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check + yarn lint:strict on the 2 touched files green.
  • Rollback pointer: republish the prior compatible group c1678d45-4aa4-49f3-8b72-448922f85705, or eas update:roll-back-to-embedded --branch preview. Source: revert 942db9a9.

2026-06-30 Preview OTA c1678d45 — symmetric chip layout + shorter approval labels

  • Change: location-first two-tier chip strip made left↔right symmetric — Row 1 [type?] [format] → [tier], Row 2 [style] [rotation?][approval?][payment?] → [end] (style moved to row-2-left as the always-present left chip). rowEndLabel = feeShown ? feeLabel : typeLabel (fee 무료/참가비 when surfaced, else 클럽); rowOneTypeLabel = feeShown ? typeLabel : null (type promoted to the row-2 end exactly when 클럽 fills it). Verified against all 6 states + the no-rotation edge. Approval labels shortened ≤2 words: 자동 승인 / 승인 필요 (en Auto-approve / Needs approval).
  • Classification: OTA-safe JS/TS — session-card.tsx, session-header.tsx, ko/en sessions.ts; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: b8458d49fbc5d62562e29d1128fc4da6ed5dd0f4.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group c1678d45-4aa4-49f3-8b72-448922f85705 (Android 019f1846-19c6-7eea-9078-633412a3694f, iOS 019f1846-19c6-7f62-b33d-63135d8cd5b9), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check + yarn lint:strict on the 4 touched files green; the layout was traced state-by-state against the 6 wireframes.
  • Rollback pointer: republish the prior compatible group bfcb4788-7f05-4e2d-aa4e-a8c875eaba33, or eas update:roll-back-to-embedded --branch preview. Source: revert b8458d49.

2026-06-30 Preview OTA bfcb4788 — session chip matrix (번개/클럽 × fee/approval)

  • Change: worked out the location-first chip-surfacing matrix. Determinants on the session: origin (번개 = clubId null / 클럽 = clubId set), participationFee (>0 = paid), publicInviteEnabled (guest-seeking), publicInviteApproval. NEW type chip (번개/클럽, Row 1 leading). 참가비 shows when pickup OR paid OR (club AND guest-seeking) — 무료 when free. 참가승인 shows when pickup OR (club AND guest-seeking). publicInviteEnabled is the semantic gate (no per-viewer membership lookup). WHEN band date/time relaxed from cardBody/600 → plain cardBody (venue stays the single bold anchor). Scoped to chipRowTwoTier; production default chipRow unchanged. New i18n: clubBadge, free.
  • Classification: OTA-safe JS/TS — session-card.tsx, session-header.tsx, ko/en sessions.ts; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: a4cfd10738920ea9e186ef8f674300b0ae679fe2.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group bfcb4788-7f05-4e2d-aa4e-a8c875eaba33 (Android 019f182c-8bb6-7b75-93a5-e9f108698d0a, iOS 019f182c-8bb6-7593-9653-04474fab425e), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check + yarn lint:strict on the 4 touched files green; the type/fee/approval gating was audited line-by-line against the matrix.
  • Smoke: install the 0.6.2 preview build (255bdfa0); QA a 번개 (always fee+approval), a free member-only club session (neither, just 클럽), and a guest-seeking club session (both) — pending owner.
  • Rollback pointer: republish the prior compatible group c56531e2-74a5-4d49-9262-ed64602e2e56, or eas update:roll-back-to-embedded --branch preview. Source: revert a4cfd107.

2026-06-30 Preview OTA c56531e2 — date/time WHEN band (location-first)

  • Change: the plain date/time line below the map becomes a soft full-width band — $surfaceSecondary strip, no border, 44px (TOUCH_TARGET.MIN), date left + time right, both cardBody/600. One a11y node spoken naturally (오전/오후, no slashes/tilde) via formatDate + formatTimeRangeKo12; visible values stay compact (2026/07/04 (토) + 19:00~21:00). No calendar/clock icons (styling canon: icons sparingly, not decorative labels next to self-describing text).
  • Classification: OTA-safe JS/TS — session-card.tsx, session-header.tsx; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: 9431d7461cbf39276373540b45a72344c5852aa9.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group c56531e2-74a5-4d49-9262-ed64602e2e56 (Android 019f1801-2adc-7ba2-8a3b-bf49dc7bd6be, iOS 019f1801-2adc-7f05-aefc-da348e7230e0), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check + yarn lint:strict on the 2 touched files green.
  • Rollback pointer: republish the prior compatible group 65ba4351-0f9d-43ba-8c2a-6962a9aed7d1, or eas update:roll-back-to-embedded --branch preview. Source: revert 9431d746.

2026-06-30 Preview OTA 65ba4351 — revert map block; date/time outside the map

  • Change: owner reverted the date-in-map experiment. The map block returns to a simple bottom bar (venue subheader + court) + standalone top-right weather at the canonical 132 height; date/time render on their own line OUTSIDE the map (below it). The two-tier chip strip is retained.
  • Classification: OTA-safe JS/TS — venue-hero.tsx, session-card.tsx, session-header.tsx; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: d49994dd869ea3f784cdf7c17014393d75d1b1e4.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 65ba4351-0f9d-43ba-8c2a-6962a9aed7d1 (Android 019f1761-f855-7284-962a-38e9c814c64b, iOS 019f1761-f855-7d61-9bb5-40df67e30c9e), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check + yarn lint:strict on the 3 touched files green.
  • Rollback pointer: republish the prior compatible group 1bb4d1a4-9d81-4cd5-844a-87f561d6913c, or eas update:roll-back-to-embedded --branch preview. Source: revert d49994dd.

2026-06-30 Preview OTA 1bb4d1a4 — venue subheader emphasis + block-row swap

  • Change: the card title is the 시/도 region (header); the venue title now carries canonical subheader emphasis (cardBody + 600, one level below cardTitle) and the map-block rows swap so it leads — Row 1 WHERE (venue subheader + court), Row 2 WHEN (date/time + weather). Reads header (시/도) → subheader (venue) → details; a single deliberate subheader instead of the flat all-uniform look.
  • Classification: OTA-safe JS/TS — venue-hero.tsx only; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: 5e9f255bd8f5b20a79203a82016283650db69089.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 1bb4d1a4-9d81-4cd5-844a-87f561d6913c (Android 019f1745-233c-7046-8ef5-2139afb87608, iOS 019f1745-233c-7174-8488-d7cf6a91e8b6), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check + yarn lint:strict on venue-hero.tsx green.
  • Smoke: install the 0.6.2 preview build (255bdfa0); confirm the venue reads as a subheader leading the block above the WHEN row — pending owner.
  • Rollback pointer: republish the prior compatible group 8feb7a9b-ef6d-4163-b77b-39bae790955c, or eas update:roll-back-to-embedded --branch preview. Source: revert 5e9f255b.

2026-06-30 Preview OTA 8feb7a9b — uniform map-block text + time-end symmetry

  • Change: per owner feedback the mixed emphasis read as noisy — the map block now renders every label in ONE style (cardMeta $text, no per-element weight/colour). Row symmetry: when the WHEN row has no weather, the time pins to the END (mirrors the WHERE row's right-pinned court); with weather, time stays beside the date and weather takes the right slot.
  • Classification: OTA-safe JS/TS — session-card.tsx, venue-hero.tsx; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: c99b3c83a20a6f74cffccc493f2b075110670c3c.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 8feb7a9b-ef6d-4163-b77b-39bae790955c (Android 019f1721-6a64-7df7-8c44-ba703cdb9af4, iOS 019f1721-6a64-7ad1-bb35-5b144a732888), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check + yarn lint:strict on the 2 touched files green.
  • Smoke: install the 0.6.2 preview build (255bdfa0); confirm the block reads as one uniform style + time right-aligns on no-weather sessions — pending owner.
  • Rollback pointer: republish the prior compatible group 96ebedb7-e209-4045-98c5-f43616c4013e, or eas update:roll-back-to-embedded --branch preview. Source: revert c99b3c83.

2026-06-30 Preview OTA 96ebedb7 — location-first two-tier chip strip

  • Change: with when/where now in the map, the location-first card's chip strip becomes two tiers — Row 1 identity (format · style → tier right), Row 2 logistics (로테이션 N분 NEW from session.rotationMinutes · approval · payment → 참가비 right). Fee chip reads the canonical session.participationFee, not the legacy costTotal split. Scoped to the dev-gated location-first layout (new chipRowTwoTier); the production default chipRow is untouched.
  • Classification: OTA-safe JS/TS — session-header.tsx, session-card.tsx; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: a76e9704523faa9f7b61c0b6ed4d0037502ae690.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 96ebedb7-e209-4045-98c5-f43616c4013e (Android 019f16ce-dc48-73d3-9294-2c07a00b9ada, iOS 019f16ce-dc48-7cfa-a32f-84727c14b111), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check (typecheck validated the new i18n paths + tests) + yarn lint:strict on the 2 touched files green; pre-push gate passed.
  • Smoke: install the 0.6.2 preview build (255bdfa0), enable sessionSummaryPolishMode on a rotation + fee session; confirm Row 1 format/style→tier and Row 2 로테이션→참가비 read cleanly without overflow — pending owner on-device.
  • Rollback pointer: republish the prior compatible group f7eadce5-c876-4393-ab8f-d689ddf99659, or eas update:roll-back-to-embedded --branch preview. Source: revert a76e9704.

2026-06-30 Preview OTA f7eadce5 — map-block emphasis hierarchy + court formatting

  • Change: text styling inside the consolidated location-first map block, grounded in the two-tier role system (cardMeta 13px/$textSecondary; $text full emphasis) and the DateTile date-hero precedent — one emphasized anchor per row (date, venue) in $text+600, rest muted cardMeta, all 13px. Court info drops the middle dot ("하드 · 2면" → "하드 2면"); count confirmed = session.courtCount(our booking). Map height156 → 168.
  • Classification: OTA-safe JS/TS — session-card.tsx, venue-hero.tsx; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: 5f53f2429b0111c230d1c16d5076566f378ab062.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group f7eadce5-c876-4393-ab8f-d689ddf99659 (Android 019f16b4-097c-7fce-b5f1-01ab34235e32, iOS 019f16b4-097c-730d-81dc-3058e1d4a588), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check + yarn lint:strict on the 2 touched files green; pre-push gate passed; guard published.
  • Smoke: install the 0.6.2 preview build (255bdfa0), enable sessionSummaryPolishMode; confirm date/venue read as the row anchors + court shows "하드 2면" — pending owner on-device.
  • Rollback pointer: republish the prior compatible group 77e93984-35e6-4eea-82d4-5e0404e6f0f5, or eas update:roll-back-to-embedded --branch preview. Source: revert 5f53f242.

2026-06-30 Preview OTA 77e93984 — consolidate location-first map overlays

  • Change: the location-first map (dev-gated sessionSummaryPolishMode) previously floated a top WHEN bar + a bottom WHERE bar over the pin (three competing focal points). Consolidated both into ONE bottom block — Row 1 WHEN (date/time left, weather right), Row 2 WHERE (venue left, court right) — so the map reads as pin + one block. Date format shortened to MM/DD (요일) via a new year-less formatMonthDaySlashWithWeekday (DATA-1 util).
  • Classification: OTA-safe JS/TS — venue-hero.tsx, session-card.tsx, date-format.ts; no native-sensitive file changed since the 0.6.2 baseline.
  • Source commit: bb2c4dcf1b51a3811b80f9497a61394e71e397be.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 77e93984-35e6-4eea-82d4-5e0404e6f0f5 (Android 019f1690-d797-7f02-83e8-73b2032af98b, iOS 019f1690-d797-7c0e-93ac-f8c4e69f1d87), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check (typecheck + tests) + yarn lint:strict on the 3 touched files green; pre-push gate passed; guard published.
  • Smoke: install the 0.6.2 preview build (255bdfa0), enable sessionSummaryPolishMode; confirm the single bottom block reads cleanly + the pin is uncluttered — pending owner on-device.
  • Rollback pointer: republish the prior compatible group d26fa9b8-3853-4389-af9e-f5d2c1dbb53b, or eas update:roll-back-to-embedded --branch preview. Source: revert bb2c4dcf.

2026-06-30 Preview OTA d26fa9b8 — date/time inside the location-first map hero

  • Change: on the dev-gated sessionSummaryPolishMode location-first SessionCard, the date/time moves off a separate pill row INTO the VenueHero map as a top-left pill, paired with the weather glance in one top bar (the WHEN strip, mirroring the bottom WHERE bar); the map height bumps 132 → 156 so the bars frame the centred pin rather than obscure it.
  • Classification: OTA-safe JS/TS client change — only .tsx presentation files (venue-hero.tsx, session-card.tsx, session-header.tsx); no native-sensitive file changed since the 0.6.2 baseline build, so the guard published.
  • Source commit: 5efd89c707ea4affe60873f8c7c2cd96a5af90cd.
  • Source sync: pushed to origin/main; left/right count 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group d26fa9b8-3853-4389-af9e-f5d2c1dbb53b (Android 019f1664-47c4-7b8e-bac4-89c39dac557a, iOS 019f1664-47c4-755b-9265-7e66644702e0), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Verification: yarn check (typecheck + 43 tests) + yarn lint:strict on the 3 touched files green; pre-push gate passed; guard published (no native-sensitive change since baseline b28a60ad).
  • Smoke: install the 0.6.2 preview build (Android 255bdfa0), enable sessionSummaryPolishMode in the dev panel; verify date/time pill legibility in the map + that the pin is not obscured at height 156 — pending owner on-device.
  • Rollback pointer: republish the prior compatible group 09616b48-fb4d-456f-85f8-1beec0435e09, or eas update:roll-back-to-embedded --branch preview. Source: revert 5efd89c7.

2026-06-29 Preview OTA 09616b48 — recruitment status + card polish + faster seed

  • Change: ship the config-driven recruitment-status feature (commit 43c21617) plus the on-device QA follow-ups (commit ebee90d6): court info renders without a venue-name overlay, every recruitment state tints the participation ring, and the QA seed is ~5× faster (lookup-first createUsers + gated deleteTestAuthUsers).
  • Classification: OTA-safe JS/TS client change — no native-sensitive files changed since the 0.6.2 baseline build, so the guard published.
  • Source commit: ebee90d6011710a485b9ef896f2070a1085172ac.
  • Source sync: pushed to origin/main; git rev-list --left-right --count origin/main...HEAD = 0 0.
  • App / runtime version: 0.6.2 (unchanged — JS-only).
  • OTA artifact: preview update group 09616b48-fb4d-456f-85f8-1beec0435e09 (Android 019f125c-1a44-7e67-aa51-47b101268dd5, iOS 019f125c-1a44-740c-a01e-6fc93a267096), branch preview. Baseline lastCompatiblePreviewOtaGroup advanced to this group.
  • Backend artifact: seed-scenario edge function redeployed (the seed perf change).
  • Verification: yarn check + yarn lint:strict green; node scripts/guard-ota-release.mjs preview passed (clean tree, synced, no native-sensitive change since baseline b28a60ad); seed re-run timing measured (recruitment_states 7.8s → 1.6s).
  • Smoke: install the 0.6.2 preview build (Android 255bdfa0) to receive the update; on-device verification of the recruitment ring + court info pending owner.
  • Rollback pointer: republish the prior compatible group 704b7bff-729a-4966-9372-aaba021f37f2, or eas update:roll-back-to-embedded --branch preview. Source: revert ebee90d6 / 43c21617.
  • Known gaps: dense_setup still ~21s (the dense world inserts, not user creation — a separate batching optimization).

2026-06-29 Preview 0.6.2 build 255bdfa0 and OTA

  • Change: cut the first compatible preview build on runtime 0.6.2 and publish the preview OTA carrying the accumulated OTA-safe client JS (location-first session card experiment, session closeout status, R1 share/deep-link receivers) that the native-compatibility guard had blocked since baseline commit 111dbf77.
  • Classification: native/runtime change (new Kakao native modules + app-link config + Expo SDK patch bumps) requiring a preview build, then an OTA-safe client-JS preview update on the new runtime.
  • Source commits: e9c30c57 bumped app version 0.6.1 -> 0.6.2 (RVO-4 native/runtime decision); b28a60ad7d3bc28d615fac5f0eec3a9a6e2ad2f2 (fix(mobile): add @react-native-kakao/social) is the build source; fa66e90be3baa47780c548da74cba0ccb493ba49 re-baselined + published the OTA.
  • Source sync: pushed to origin/main; git rev-list --left-right --count origin/main...HEAD returned 0 0 at each commit.
  • App version: 0.6.1 -> 0.6.2. The bump gives the new build a fresh runtime bucket so this OTA cannot target old 0.6.1 installs.
  • Runtime version: 0.6.2 (runtimeVersion.policy = appVersion).
  • EAS build artifacts:
    • 773eca7e-b687-4ccb-8003-7cb61f29a9dd - ERRORED (Run gradlew).
    • abf5d4d2-d064-4152-9878-df00e098df57 - ERRORED (Run gradlew, same cause).
    • 255bdfa0-9f1c-4681-8bf6-c60c9c340cc7 - FINISHED, Android APK, runtime 0.6.2, commit b28a60ad. Install: https://expo.dev/artifacts/eas/MuJ2LP3_7rYZiJKKrkHOHPa52LBSVEo94j3-dK_dqhg.apk.
  • Build failure root cause: @react-native-kakao/share@2.4.5's android/build.gradle does api getWorkspaceProject("social") - a hard native dependency on @react-native-kakao/social that the library omits from its npm peerDependencies (declares only core). The first two builds failed evaluating :react-native-kakao_share ("Could not find the react-native-kakao/social package"), decoded from the brotli-compressed EAS gradle log. Fixed by adding @react-native-kakao/social@2.4.5 (commit b28a60ad); social itself needs only core, so the native graph is complete.
  • OTA artifact: preview update group 704b7bff-729a-4966-9372-aaba021f37f2 (Android update 019f10ff-dca9-7b09-b2a9-db4e38258699, iOS update 019f10ff-dca9-7d23-a048-ab14c20ff1fe), runtime 0.6.2, branch preview, published from commit fa66e90b. Dashboard: https://expo.dev/accounts/lbsky/projects/twomore-v2/updates/704b7bff-729a-4966-9372-aaba021f37f2.
  • Backend artifact: none.
  • Verification: node scripts/guard-ota-release.mjs preview passed (clean tree, synced, no native-sensitive change since the re-baselined commit b28a60ad); each commit cleared the full pre-push gate (yarn docs:check && yarn typecheck && yarn test && yarn lint:strict && yarn lint:agents-md && yarn verify:no-stale-claims).
  • Smoke evidence: owner installed APK 255bdfa0 on an Android device and confirmed boot + core app function (2026-06-29). Kakao share is intentionally not exercised (template IDs + native key remain deferred R4 gates).
  • Interpretation: the venue-first card OTA recorded in the prior blocked entry, plus the location-first card / closeout / R1 share-receiver JS, are now distributable on the 0.6.2 preview runtime.
  • Rollback pointer: republish the prior compatible preview update group, or eas update:roll-back-to-embedded --branch preview. Source rollback: revert b28a60ad (+ re-baseline) to drop the Kakao social dep.
  • Known gaps: iOS preview build not produced (EXT-4, no Apple credentials); Kakao share native key + template IDs remain deferred (EXT-14); the react-native-kakao/social peer omission is an upstream library packaging gap worth reporting upstream.

2026-06-29 - Venue-First Session Card Polish Preview OTA Blocked

  • Change: polish the dev-gated venue-first session summary card so the weather glance sits at the top right of the venue hero, the date/time row renders YYYY/MM/DD (요일) on the left and HH:MM~HH:MM on the right, metadata chips remain below the hero, and the canonical participation row renders below the chips with capacity ring, participation text, avatars, status chip, chevron, and expandable match summary.
  • Classification: OTA-safe JS/TS client behavior, but the preview OTA attempt is blocked by the existing native-compatibility baseline guard before EAS publishing.
  • Source commit: 1113d4b508012226d1e5854d6fed15a5d3ded8bc (feat(app): polish venue-first session card).
  • Source sync: pushed to origin/main; git status --short --branch showed ## main...origin/main.
  • Source sync proof: node scripts/guard-ota-release.mjs preview passed the clean-tree/upstream-sync gate for commit 1113d4b5, then stopped before EAS publish on native baseline compatibility.
  • Target: blocked preview OTA; no preview OTA, preview build, backend deploy, or production artifact was created.
  • App version: unchanged at current 0.6.2.
  • Runtime version: unchanged at current 0.6.2 because runtimeVersion.policy = appVersion.
  • EAS artifact: none. The guard refused publish before eas update.
  • Backend artifact: none.
  • Verification:
    • yarn workspace @twomore/app test src/presentation/utils/__tests__/date-format.test.ts --runInBand
    • yarn workspace @twomore/app typecheck
    • yarn lint:strict
    • git diff --check
    • yarn check
    • pre-push hook: yarn docs:check && yarn typecheck && yarn test && yarn lint:strict && yarn lint:agents-md && yarn verify:no-stale-claims
    • node scripts/guard-ota-release.mjs preview
  • Smoke evidence: not run. No installable preview OTA artifact exists for this source state.
  • Guard blocker output: preview OTA was refused because native-sensitive files changed after baseline commit 111dbf77eb590b2ee6a49b0c70bffc3c2a5a342b; compatible Android build 6aa365f6-7814-4250-a708-a27843a919dc; compatible iOS build not recorded. The changed native-sensitive files named by the guard were apps/mobile/app.config.js, apps/mobile/app.json, apps/mobile/package.json, package.json, packages/app/package.json, and yarn.lock.
  • Interpretation: the venue-first card source change is safely version-controlled and validated, but cannot be distributed by OTA until a compatible preview build is created, smoke-tested, and recorded in apps/mobile/ota-compatibility-baseline.json.
  • Rollback pointer: revert source commit 1113d4b5 to remove the app change. No OTA rollback exists because no update group was published.
  • Known gaps: create and smoke a compatible preview build, update the OTA compatibility baseline with the proven build/source state, rerun node scripts/guard-ota-release.mjs preview, then publish yarn ota:preview.

2026-06-29 - R1 External Gates Deferred To Production Readiness

  • Change: document that remaining external R1 credentials, live-domain association deployment, compatible preview build/baseline advancement, iOS preview build, and device-smoke proof are deferred to Production Readiness/R4.
  • Classification: docs-only release routing. This changes no mobile runtime behavior, backend schema, edge function, OTA artifact, preview build, or production artifact.
  • Source commit: 145bea94fd31ac73b92493270f201f548dfaf446 (docs(release): defer external r1 gates to r4).
  • Source sync: pushed to origin/main with this ledger handoff.
  • Target: docs only; no preview OTA, preview build, backend deploy, or production artifact.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • yarn report:r1-release-readiness
    • yarn docs:check
    • git diff --check
  • Current blocker posture: yarn report:r1-release-readiness still reports missing app-link env, missing Kakao template env, live app-link redirect, Kakao credential/native proof, preview OTA/native compatibility, EAS preview env, stale Android preview build compatibility, and absent iOS preview build evidence.
  • Interpretation: these blockers remain real launch gates, but they are no longer active R1 source-loop work unless the owner provides the required credentials, hosting/domain access, build authority, or device-smoke environment.
  • Rollback pointer: revert commit 145bea94fd31ac73b92493270f201f548dfaf446 if R1 should again block on external credential/domain/build closure before any further R2/R3 work.
  • Known gaps: R4 Production Readiness must still close or explicitly sign off the external gates before public launch.

2026-06-28 - Session Closeout Status Preview OTA Blocked

  • Change: add a shared session closeout selector, surface completed-session recap badges for clean, score-review, and score-issue states, and make the session-detail end-event confirmation copy distinguish normal closeout, unfinished matches, and no recorded matches.
  • Classification: OTA-safe JS/TS client behavior, but the preview OTA attempt is blocked by the existing native-compatibility baseline guard before EAS publishing.
  • Source commit: e717c3c133e373a6df6256a366e66b0eaf4b4b4e (feat(session): surface closeout status).
  • Source sync: pushed to origin/main; git status --short --branch showed ## main...origin/main.
  • Source sync proof: node scripts/guard-ota-release.mjs preview passed the clean-tree/upstream-sync gate for commit e717c3c1, then stopped before EAS publish on native baseline compatibility.
  • Target: blocked preview OTA; no preview OTA, preview build, backend deploy, or production artifact was created.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none. The guard refused publish before eas update.
  • Verification:
    • yarn workspace @twomore/app test src/presentation/utils/__tests__/session-closeout.test.ts --runInBand
    • yarn workspace @twomore/app typecheck
    • yarn check
    • pre-push hook: yarn docs:check && yarn typecheck && yarn test && yarn lint:strict && yarn lint:agents-md && yarn verify:no-stale-claims
    • yarn report:r1-release-readiness
    • node scripts/guard-ota-release.mjs preview
  • Smoke evidence: not run. No installable preview OTA artifact exists for this source state.
  • R1 readiness result: source contracts, env-example coverage, and hosted evidence workflow contract passed. R1 remains blocked on app-link proof env, Kakao template env, live app-link redirect, Kakao credential/native proof, preview OTA/native compatibility, EAS preview env names, Android preview build compatibility, and missing iOS preview build.
  • Guard blocker output: preview OTA was refused because native-sensitive files changed after baseline commit 111dbf77eb590b2ee6a49b0c70bffc3c2a5a342b; compatible Android build 6aa365f6-7814-4250-a708-a27843a919dc; compatible iOS build not recorded. The changed native-sensitive files named by the guard were apps/mobile/app.config.js, apps/mobile/app.json, apps/mobile/package.json, package.json, packages/app/package.json, and yarn.lock.
  • Interpretation: the closeout-status source change is safely version-controlled and validated, but cannot be distributed by OTA until a compatible preview build is created, smoke-tested, and recorded in apps/mobile/ota-compatibility-baseline.json.
  • Rollback pointer: revert source commit e717c3c1 to remove the app change. No OTA rollback exists because no update group was published.
  • Known gaps: create and smoke a compatible preview build, update the OTA compatibility baseline with the proven build/source state, rerun node scripts/guard-ota-release.mjs preview, then publish yarn ota:preview.

2026-06-28 - Location-First Session Card Preview OTA Blocked

  • Change: add a dev-gated location-first session summary card experiment that moves location/save action above the map, overlays the venue on the hero, moves date/time beside weather, and condenses participation into the chip row.
  • Classification: OTA-safe JS/TS client behavior, but the preview OTA attempt is blocked by the existing native-compatibility baseline guard before EAS publishing.
  • Source commit: 7b5f9ea1f1a7fbc4c2535561e77ad95e216896d1 (feat(app): add location-first session card experiment).
  • Source sync: pushed to origin/main; git status --short --branch showed ## main...origin/main, and git log --oneline @{u}..HEAD was empty.
  • Source sync proof: node scripts/guard-ota-release.mjs preview passed the clean-tree/upstream-sync gate for commit 7b5f9ea1, then stopped before EAS publish on native baseline compatibility.
  • Target: blocked preview OTA; no preview OTA, preview build, backend deploy, or production artifact was created.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none. The guard refused publish before eas update.
  • Verification:
    • yarn workspace @twomore/app typecheck
    • targeted store/status tests including src/presentation/stores/__tests__/dev-panel-prefs.store.test.ts
    • git diff --check
    • yarn lint:strict
    • yarn check
    • pre-push hook: yarn docs:check && yarn typecheck && yarn test && yarn lint:strict && yarn lint:agents-md && yarn verify:no-stale-claims
  • Smoke evidence: not run. No installable preview OTA artifact exists for this source state.
  • Guard blocker output: preview OTA was refused because native-sensitive files changed after baseline commit 111dbf77eb590b2ee6a49b0c70bffc3c2a5a342b; compatible Android build 6aa365f6-7814-4250-a708-a27843a919dc; compatible iOS build not recorded. The changed native-sensitive files named by the guard were apps/mobile/app.config.js, apps/mobile/app.json, apps/mobile/package.json, package.json, packages/app/package.json, and yarn.lock.
  • Interpretation: the session-card source change is safely version-controlled, but cannot be distributed by OTA until a compatible preview build is created, smoke-tested, and recorded in apps/mobile/ota-compatibility-baseline.json.
  • Rollback pointer: revert source commit 7b5f9ea1 to remove the app change. No OTA rollback exists because no update group was published.
  • Known gaps: create and smoke a compatible preview build, update the OTA compatibility baseline with the proven build/source state, rerun node scripts/guard-ota-release.mjs preview, then publish yarn ota:preview.

2026-06-28 - R1 Readiness Local Env Loader

  • Change: make scripts/check-r1-release-readiness.mjs load local env files (.env, apps/mobile/.env, apps/web/.env, and apps/web/.env.local) as defaults before running child gates. Explicit shell variables still take precedence, and report output still prints only env names/status, never values.
  • Classification: release tooling only. It changes no mobile runtime behavior, backend schema, edge function, OTA artifact, preview build, or production artifact.
  • Source commit: ca0c59ce904d53d19dca82bba5a3087826828fb6 (chore(release): load local env in r1 readiness gate).
  • Source sync: source commit and this evidence handoff are pushed together before final handoff.
  • Target: source/tooling/docs only; no preview OTA, preview build, backend deploy, or production artifact.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • node --check scripts/check-r1-release-readiness.mjs
    • node scripts/check-r1-release-readiness.mjs --report
  • Report output: local Kakao native app-key proof now passes when the existing local EXPO_PUBLIC_KAKAO_APP_KEY env file value is available. R1 remains blocked on missing app-link proof env, missing six Kakao template IDs, live .well-known redirect, OTA/native compatibility, EAS preview env, and platform build/device smoke gates.
  • Interpretation: the readiness report now reflects actual local app/build env files instead of requiring every local value to be manually exported in the shell.
  • Rollback pointer: revert source commit ca0c59ce904d53d19dca82bba5a3087826828fb6. No OTA/build rollback exists because no artifact was published.
  • Known gaps: same R1 external gates as the aggregate report.

2026-06-28 - R1 Env Setup Coverage Guard

  • Change: add the six R1 KakaoTalk Share template env names to .env.example, add app-link server env names to apps/web/.env.example, document the Kakao template env setup in credential/build guides, and extend scripts/check-r1-release-readiness.mjs with an env-example coverage check.
  • Classification: release setup/tooling and documentation only. It changes no mobile runtime behavior, backend schema, edge function, OTA artifact, preview build, or production artifact.
  • Source commit: 81c9909e4fc3c78287c0ffdd0c5df7ae1fd14e8b (chore(release): guard r1 env setup examples).
  • Source sync: pushed to origin/main in the docs/release handoff ending at 2fba00b2.
  • Target: source/tooling/docs only; no preview OTA, preview build, backend deploy, or production artifact.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • node --check scripts/check-r1-release-readiness.mjs
    • node scripts/check-r1-release-readiness.mjs --report
    • yarn docs:check
  • Report output: R1 env example coverage passes and reports that R1 app-link and Kakao env names are listed in env examples. R1 remains blocked on the actual missing credentials, live app-link redirect, preview native compatibility, EAS preview env, and platform build/device smoke gates.
  • Interpretation: the remaining R1 credential/setup variables are no longer only documented in prose; the aggregate readiness gate now guards the env examples that future agents and operators copy from.
  • Rollback pointer: revert source commit 81c9909e4fc3c78287c0ffdd0c5df7ae1fd14e8b to remove the env-example coverage guard. No OTA/build rollback exists because no artifact was published.
  • Known gaps: same R1 external gates as the aggregate report.

2026-06-28 - R1 Release Readiness Aggregate Gate

  • Change: add scripts/check-r1-release-readiness.mjs plus yarn check:r1-release-readiness, yarn check:r1-release-readiness:eas, and yarn report:r1-release-readiness.
  • Classification: release/evidence tooling only. It changes no mobile runtime behavior, backend schema, edge function, OTA artifact, preview build, or production artifact.
  • Source commit: 64ea165c7651226f4f9a316b7de581d1cc353a92 (chore(release): add r1 readiness gate).
  • Source sync: source commit and this docs handoff are pushed together before final handoff. The first local report correctly showed the source commit as one local commit ahead of origin/main until that push happened.
  • Target: source/tooling only; no preview OTA, preview build, backend deploy, or production artifact.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • node --check scripts/check-r1-release-readiness.mjs
    • node scripts/check-r1-release-readiness.mjs --eas --report
  • Report output, 2026-06-28: static app-link source contract, Kakao share source contract, and hosted evidence workflow contract pass. R1 release readiness remains blocked by missing local/web app-link proof env (TWOMORE_APPLE_TEAM_ID, TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS), missing local Kakao template IDs, missing Kakao native app key, live https://twomore.app/.well-known/apple-app-site-association redirecting to www, missing EAS preview Kakao env names, Android preview build 6aa365f6-7814-4250-a708-a27843a919dc being behind native-sensitive source, and no iOS preview build returned by EAS.
  • Additional Android app-link evidence, 2026-06-28: downloaded the APK artifact for Android preview build 6aa365f6-7814-4250-a708-a27843a919dc and ran apksigner verify --print-certs. Its public signing certificate SHA-256 fingerprint is F4:67:1F:16:B3:95:12:06:0C:20:4C:E4:5E:AA:79:F8:6A:C1:AD:C9:29:49:F4:D8:AE:EB:DB:6A:D3:D2:8B:3E. This reduces the Android identifier unknown, but it is not configured in the live web deploy env and should be rechecked on the final candidate build if signing credentials rotate.
  • Interpretation: future agents no longer need to reconstruct R1 readiness from separate docs, curl calls, EAS commands, and local env checks. yarn check:r1-release-readiness:eas is the strict R1 acceptance gate; yarn report:r1-release-readiness is the handoff-safe status command.
  • Rollback pointer: revert source commit 64ea165c7651226f4f9a316b7de581d1cc353a92 to remove the aggregate gate. No OTA/build rollback exists because no artifact was published.
  • Known gaps: same as the report output; after credentials/live-domain/build state are fixed, rerun the strict gate and record the pass before accepting R1-3/R1-4.

2026-06-28 - Preview OTA Native Compatibility Guard

  • Change: add apps/mobile/ota-compatibility-baseline.json and extend scripts/guard-ota-release.mjs so preview OTA fails when native-sensitive files changed after the last compatible preview build/source baseline.
  • Classification: release guard/tooling plus documentation. It changes no app runtime behavior, backend schema, edge function, OTA artifact, preview build, or production artifact.
  • Source commit: 22f214ff10d32bdc054ed74d125dc40103e5fca9 (chore(release): guard ota native compatibility).
  • Source sync: pushed to origin/main before this evidence entry.
  • Target: source/tooling only; no preview OTA, preview build, backend deploy, or production artifact.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • node --check scripts/guard-ota-release.mjs
    • yarn docs:check
    • yarn workspace @twomore/mobile preflight
    • yarn check
    • pre-push hook
    • expected-failing node scripts/guard-ota-release.mjs preview from clean, synced 22f214ff
  • Expected-failing guard output: source sync passes, then preview OTA is refused because native-sensitive files changed after baseline 111dbf77eb590b2ee6a49b0c70bffc3c2a5a342b / compatible Android build 6aa365f6-7814-4250-a708-a27843a919dc.
  • Changed native-sensitive files named by the guard: apps/mobile/app.config.js, apps/mobile/app.json, apps/mobile/package.json, package.json, packages/app/package.json, and yarn.lock.
  • Interpretation: this mechanically prevents an unsafe R1 preview OTA from current source. R1-3 still needs the credentialed app-link/Kakao preview build and device smoke; after that build is installed and proven, advance apps/mobile/ota-compatibility-baseline.json before future preview OTA.
  • Rollback pointer: revert source commit 22f214ff10d32bdc054ed74d125dc40103e5fca9 to remove the native-compatibility OTA block. There is no OTA/build rollback because no artifact was published.
  • Known gaps: Apple Team ID, Android SHA-256 fingerprints, live twomore.app/.well-known deployment without redirects, Kakao template IDs, Kakao native app key, compatible preview build, app-link/Kakao device smoke, and a green consolidated R1 hosted evidence packet.

2026-06-28 - R1 Hosted Evidence Packet Workflow

  • Change: add apps/mobile/.eas/workflows/maestro-r1-evidence-rerun.yml, a manual-dispatch hosted R1 packet that reuses one Android preview build and runs the existing R1 proof groups sequentially: seeded 01/02/08/09, seeded 10, then generated R1 share deep links.
  • Change: add scripts/check-r1-evidence-workflow.mjs and yarn check:r1-evidence-workflow so the full build workflow, build-reuse workflow, targeted R1 workflows, and consolidated packet cannot silently drift apart.
  • Classification: release/evidence workflow tooling and documentation only. It changes no mobile runtime, native config, backend schema, edge function, OTA artifact, or user-facing behavior.
  • Source commit: 4aecec580788a13bed7eec198bec2d48a6591306.
  • Source sync: source commit pushed to origin/main; ledger correction followed in cf0ed805.
  • Target: source/tooling only; no preview OTA, preview build, backend deploy, or production artifact.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none in this slice. The next R1-4 attempt should run cd apps/mobile && eas workflow:run .eas/workflows/maestro-r1-evidence-rerun.yml -F build_id=<id>.
  • Backend artifact: none.
  • Verification:
    • node --check scripts/check-r1-evidence-workflow.mjs
    • yarn check:r1-evidence-workflow
    • yarn docs:check
    • yarn check
  • Interpretation: this creates the canonical hosted R1 packet path and makes its coverage mechanically checkable, but it does not itself prove R1-4. R1-4 still needs a green packet run against the candidate preview build/OTA.
  • Rollback pointer: revert source commit 4aecec580788a13bed7eec198bec2d48a6591306 to remove the packet workflow and static guard. No OTA/build rollback exists because no artifact was published.
  • Known gaps: run the consolidated packet on the correct preview build/source state, close external Kakao/app-link gates, and record the resulting hosted run ID(s) before accepting R1.
  • Change: repair scripts/check-r1-app-links.mjs remote Android validation by adding the missing comma-list parser used for TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS.
  • Classification: release/evidence tooling only. It changes no mobile runtime, native config, backend schema, edge function, OTA artifact, or user-facing behavior.
  • Source commit: ee49c9d75f59cc6033596d75c45f76b22b85d13f (fix(release): repair app-link remote fingerprint parsing).
  • Source sync: pushed with this evidence handoff.
  • Target: source/tooling only; no preview OTA, preview build, backend deploy, or production artifact.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • node --check scripts/check-r1-app-links.mjs
    • yarn check:r1-app-links
    • local HTTP fixture for yarn check:r1-app-links:remote using loopback Apple and Android association JSON with comma-separated fingerprints
    • expected-failing live env TWOMORE_APPLE_TEAM_ID=TEAMID1234 TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS=AA:BB:CC yarn check:r1-app-links:remote
  • Local fixture proof output: remote association proof succeeds against loopback JSON, which exercises the previously hidden Android fingerprint parser path.
  • Live proof output: still fails before Android validation because https://twomore.app/.well-known/apple-app-site-association returns HTTP 307 to https://www.twomore.app/.well-known/apple-app-site-association, and the www target currently redirects .well-known requests to /auth/login.
  • Interpretation: this closes a local guard bug that would have appeared after the Apple/live-domain blocker is fixed. It does not prove live Universal Links or Android App Links.
  • Rollback pointer: revert source commit ee49c9d75f59cc6033596d75c45f76b22b85d13f to return the remote guard to its prior behavior. No OTA/build rollback exists because no artifact was published.
  • Known gaps: correct live twomore.app association deployment, Apple Team ID, Android SHA-256 fingerprints, preview build, and iOS/Android HTTP app-link device smoke.

2026-06-28 - R1-3 Kakao Native Key Env Compatibility

  • Change: make the mobile Kakao core plugin and R1 Kakao share static guard accept EXPO_PUBLIC_KAKAO_APP_KEY as a backward-compatible fallback for the preferred TWOMORE_KAKAO_NATIVE_APP_KEY build-time variable.
  • Classification: source/config guard refinement. This changes native build configuration input resolution and release verification messaging, but it does not publish an OTA, preview build, backend deploy, or production artifact.
  • Source commit: 6e40fe7ae77606946741d73ba06a6c964d41ec0e (chore(release): align kakao native key gate).
  • Source sync: pushed with evidence handoff.
  • Target: source only; no preview OTA, preview build, backend deploy, or production artifact in this slice.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • node --check scripts/check-r1-kakao-share.mjs
    • yarn check:r1-kakao-share
    • expected-failing env EXPO_PUBLIC_KAKAO_APP_KEY=placeholder yarn check:r1-kakao-share:credentials
    • yarn docs:check
    • yarn check
  • Static proof output: normal mode reports the six missing TWOMORE_KAKAO_TEMPLATE_* IDs plus TWOMORE_KAKAO_NATIVE_APP_KEY or EXPO_PUBLIC_KAKAO_APP_KEY as the native app key gate. Credential mode with only EXPO_PUBLIC_KAKAO_APP_KEY exported fails only on missing template IDs, proving the fallback satisfies the native app key check.
  • Interpretation: this removes a duplicate credential trap between existing Kakao login env setup and R1 Kakao Share native plugin setup. It does not prove Kakao-native delivery.
  • Rollback pointer: revert source commit 6e40fe7ae77606946741d73ba06a6c964d41ec0e to require only TWOMORE_KAKAO_NATIVE_APP_KEY again. No OTA/build rollback exists because no artifact was published.
  • Known gaps: approved Kakao template IDs, a preview build with the Kakao native app key and template IDs, KakaoTalk iOS/Android rendering/return smoke, correct live twomore.app association deployment, and credentialed HTTP app-link proof.

2026-06-28 - R1-3 Expo SDK 55 Preflight Alignment

  • Change: align mobile Expo SDK 55 patch dependencies to the versions expected by expo install --check: expo, expo-dev-client, expo-image-picker, expo-linking, expo-location, expo-splash-screen, expo-system-ui, and expo-updates.
  • Classification: native-build readiness dependency alignment. This changes package metadata and lockfile resolution but does not publish an OTA, preview build, backend deploy, or production artifact.
  • Source commit: daf2a01d6244578a9d5af3eef4000bfd133556ac (chore(mobile): align expo sdk patch versions).
  • Source sync: pushed with this evidence handoff.
  • Target: source only; no preview OTA, preview build, backend deploy, or production artifact in this slice.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • yarn workspace @twomore/mobile preflight
    • yarn workspace @twomore/mobile typecheck
    • git diff --check
  • Preflight proof output: Dependencies are up to date.
  • Interpretation: the Expo patch-drift blocker recorded in prior R1 evidence is closed locally. R1-3 still needs Kakao template IDs, TWOMORE_KAKAO_NATIVE_APP_KEY, preview build proof, KakaoTalk device smoke, and credentialed HTTP app-link proof.
  • Rollback pointer: revert source commit daf2a01d to restore the previous SDK patch ranges. No OTA/build rollback exists because no artifact was published.
  • Known gaps: external Kakao/app-link credentials, a preview build with those credentials, and iOS/Android device smoke.

2026-06-28 - R1-3 Kakao Native Share Adapter Wiring

  • Change: install @react-native-kakao/core and @react-native-kakao/share, configure the Expo Kakao core plugin behind TWOMORE_KAKAO_NATIVE_APP_KEY, expose configured template IDs through Expo extra, and update the native share adapter to try Kakao custom-template sharing before falling back to the OS share sheet.
  • Classification: native dependency/config plus JS adapter wiring. This is not OTA-acceptable as a finished release artifact; it requires a preview build before device proof.
  • Source commit: fb2fd1bd76835a6921f1623b8c9600da2b3f6796 (feat(app): wire kakao share adapter).
  • Source sync: pushed with this evidence handoff.
  • Target: source only; no preview OTA, preview build, backend deploy, or production artifact in this slice.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • node --check scripts/check-r1-kakao-share.mjs
    • yarn check:r1-kakao-share
    • yarn workspace @twomore/app test src/adapters/share/__tests__/share.native.test.ts --runInBand
    • yarn workspace @twomore/app typecheck
    • git diff --check
  • Build-preflight follow-up:
    • Closed by source commit daf2a01d6244578a9d5af3eef4000bfd133556ac; yarn workspace @twomore/mobile preflight now reports Dependencies are up to date.
  • Static proof output: yarn check:r1-kakao-share passes and reports the six missing TWOMORE_KAKAO_TEMPLATE_* env vars plus a Kakao native app key as the remaining Kakao gates. The preferred R1 build-time env is TWOMORE_KAKAO_NATIVE_APP_KEY; EXPO_PUBLIC_KAKAO_APP_KEY remains accepted as a backward-compatible fallback when it carries the same Kakao Native App Key.
  • Interpretation: native Kakao Share is now wired as a fallback-safe adapter, but R1-3 still cannot claim Kakao-native delivery until approved template IDs, native app key, preview build, and KakaoTalk device smoke exist.
  • Rollback pointer: revert source commit fb2fd1bd to remove the native dependency/config and return native sharing to the OS share sheet only. No OTA/build rollback exists because no artifact was published.
  • Known gaps: create/approve Kakao Developers share templates, set the six TWOMORE_KAKAO_TEMPLATE_* IDs and a Kakao native app key in the preview build environment, create a preview build that includes the native plugin/dependencies, and run iOS/Android KakaoTalk smoke for template rendering plus recipient return.
  • Source detail: R1 Activation Source Pack.

2026-06-28 - R1-3 Kakao Share Template Contract

  • Change: add the R1 Kakao custom-template manifest, attach kakaoTemplate metadata to session, club invite, match result, tier promotion, stat-card, and leaderboard share composers, extend share-content schema coverage, and add yarn check:r1-kakao-share.
  • Classification: JS/source contract plus release guard. This changes metadata carried by share-card objects, but it does not replace the current OS/web share adapters, add a native Kakao share module, or publish KakaoTalk rendering behavior.
  • Source commit: 422037f28e815f17c179f54d2a033ac40bda3c60 (feat(app): add r1 kakao template contract).
  • Source sync: pushed with this evidence handoff.
  • Target: source/static guard only; no preview OTA, preview build, backend deploy, or production artifact.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • node --check scripts/check-r1-kakao-share.mjs
    • yarn check:r1-kakao-share
    • expected-failing yarn check:r1-kakao-share:credentials
    • yarn workspace @twomore/app typecheck
    • yarn workspace @twomore/app test src/presentation/utils/__tests__/share-cards.test.ts --runInBand
    • git diff --check
  • Static proof output: yarn check:r1-kakao-share passes and reports the missing template env vars and native app key as remaining R1 Kakao gates after the follow-up native-adapter slice.
  • Credential proof output: yarn check:r1-kakao-share:credentials fails until TWOMORE_KAKAO_TEMPLATE_SESSION_INVITE, TWOMORE_KAKAO_TEMPLATE_CLUB_INVITE, TWOMORE_KAKAO_TEMPLATE_MATCH_RESULT, TWOMORE_KAKAO_TEMPLATE_TIER_PROMOTION, TWOMORE_KAKAO_TEMPLATE_STAT_CARD, TWOMORE_KAKAO_TEMPLATE_LEADERBOARD, and TWOMORE_KAKAO_NATIVE_APP_KEY are available.
  • Interpretation: the Kakao share plan is no longer a manual doc claim; source coverage is guarded. The follow-up native-adapter entry records adapter wiring; R1-3 still cannot claim Kakao-native delivery until the approved template IDs and native app key are configured in a preview build and KakaoTalk rendering/return is smoke-tested.
  • Rollback pointer: revert source commit 422037f2 if the metadata contract must be removed. No OTA/build rollback exists because no app artifact was published.
  • Known gaps: create/approve Kakao Developers share templates, set the six TWOMORE_KAKAO_TEMPLATE_* IDs, set TWOMORE_KAKAO_NATIVE_APP_KEY, create the required preview build, and run KakaoTalk device smoke for template rendering plus recipient return.
  • Source detail: R1 Activation Source Pack.
  • Change: extend scripts/check-r1-app-links.mjs with --remote mode and add root script yarn check:r1-app-links:remote.

  • Classification: release/evidence tooling only. It changes no mobile runtime, native config, backend schema, edge function, or user-facing behavior.

  • Source commit: e6f60c2850dbff3dfc6a58a6642e0f1e6c3b240c (test(release): add r1 remote app-link proof).

  • Target: source/tooling only; no preview OTA, preview build, backend deploy, or production artifact.

  • App version: unchanged at 0.6.1.

  • Runtime version: unchanged at 0.6.1.

  • EAS artifact: none.

  • Backend artifact: none.

  • Verification:

    • node --check scripts/check-r1-app-links.mjs
    • yarn check:r1-app-links
    • expected-failing yarn check:r1-app-links:remote
    • git diff --check
  • Static proof output: yarn check:r1-app-links passes and reports TWOMORE_APPLE_TEAM_ID and TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS as remaining external gates.

  • Remote proof output: yarn check:r1-app-links:remote fails with:

    text
    https://twomore.app/.well-known/apple-app-site-association must not redirect; got HTTP 307 -> https://www.twomore.app/.well-known/apple-app-site-association
  • Interpretation: the credentialed app-link proof is now mechanically gated, and the current blocker is broader than missing Apple/Android identifiers: the canonical domain is not serving this app's .well-known association JSON.

  • Rollback pointer: revert source commit e6f60c28 if the remote guard creates operational noise. No OTA/build rollback exists because no app artifact was published.

  • Known gaps: fix the live twomore.app deployment, set Apple Team ID and Android SHA-256 fingerprints in the web deploy environment, pass yarn check:r1-app-links:remote, create a preview build, then run iOS/Android HTTP app-link device smoke. Kakao-native share template proof is still pending.

  • Source detail: R1 Activation Source Pack.

  • Change: extend scripts/prepare-r1-share-deep-link-flow.mjs so the generated R1-3 Maestro flow also opens twomore://join-club?code=<inviteCode>, asserts the join screen and prefilled code, and submits without typing.
  • Classification: test/evidence tooling only. No native config, runtime, database, edge-function, or mobile bundle behavior change.
  • Source commit: c9d81257205f29cf71707eb730d762aab65876d5 (test(mobile): add invite link to r1 deeplink flow).
  • Source sync: pushed to origin/main before hosted workflow trigger.
  • Target: hosted Android Maestro rerun against an existing preview build; no preview OTA/build.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • Compatible preview build: Android build 6aa365f6-7814-4250-a708-a27843a919dc, channel preview, runtime 0.6.1.
  • EAS artifact: hosted workflow 019f0c63-85bc-7f19-9d9b-d449ed548eb1.
  • Backend artifact: none; reuses the deployed seed-scenario club_growth fixture and its shareInviteCode = JOINR12 summary field.
  • Hosted pass evidence: job 019f0c63-865b-797f-b6ad-5dcfaa4221e2 finished SUCCESS; JUnit artifact 019f0c67-620e-7a41-a623-64c95676e217 reports tests=1, failures=0, time=43.0; Maestro results artifact 019f0c67-6014-71f5-bdd1-7e9c4f533429 includes command traces for all generated share links.
  • Smoke evidence: command trace opened twomore://join-club?code=JOINR12, asserted 클럽 가입하기, asserted JOINR12, tapped club.join-code.submit, and asserted 중랑 초대코드 조인 테스트.
  • Verification:
    • node --check scripts/prepare-r1-share-deep-link-flow.mjs
    • yarn prepare:r1-share-deep-link-flow
    • pre-push yarn check
    • hosted workflow 019f0c63-85bc-7f19-9d9b-d449ed548eb1
  • Monitoring checked: not applicable; pre-production hosted smoke only.
  • Rollback pointer: revert source commit c9d81257 if the generated invite-link smoke must be removed. No OTA/build rollback exists because no app artifact was published.
  • Known gaps: R1-3 remains active for Kakao-native share template proof and credentialed HTTP iOS Universal Links / Android App Links preview-build proof.
  • Source detail: R1 Activation Source Pack.
  • Change: make https://twomore.app the canonical R1 share-link domain, add iOS Associated Domain config for applinks:twomore.app, add Android autoVerify intent filters for /s, /m, /p, /lb, and /join-club, serve credential-gated .well-known/apple-app-site-association and .well-known/assetlinks.json handlers from the web app, and add yarn check:r1-app-links to the normal verification ladder.
  • Classification: native config plus web route/source config. This is not OTA-safe and does not itself publish an OTA/build artifact; the native app-link config requires a preview build after the external identifiers are supplied.
  • Source commit: a667567887143fbc85bb801ffad46e187dd6b222 (feat(mobile): wire r1 app link config).
  • Source sync: pushed with this evidence handoff.
  • Target: source/web/static config only; no preview OTA, preview build, backend deploy, or production artifact.
  • App version: unchanged at 0.6.1.
  • Runtime version: unchanged at 0.6.1.
  • EAS artifact: none.
  • Backend artifact: none.
  • Verification:
    • yarn check:r1-app-links
    • git diff --check
    • yarn workspace @twomore/web typecheck
    • yarn workspace @twomore/app test src/presentation/utils/__tests__/share-cards.test.ts --runInBand
    • yarn workspace @twomore/mobile typecheck
    • yarn workspace @twomore/web build
  • Build evidence: the web build emitted dynamic routes for /.well-known/apple-app-site-association and /.well-known/assetlinks.json. It still printed the existing non-blocking expo-location export warning.
  • Guard behavior: yarn check:r1-app-links passes the static source/config contract and reports the remaining external gates: TWOMORE_APPLE_TEAM_ID and TWOMORE_ANDROID_SHA256_CERT_FINGERPRINTS.
  • Known gaps: no credentialed AASA or Digital Asset Links output can be claimed until those identifiers are set in the web deploy environment; no HTTP app-link recipient-return proof can be claimed until a preview build is installed and device/browser smoke verifies https://twomore.app/... on iOS and Android. Kakao-native share template proof is still pending.
  • Rollback pointer: revert source commit a6675678 if the static app-link config must be removed. There is no OTA rollback because no app artifact was published.
  • Source detail: R1 Activation Source Pack.

2026-06-28 - R1-3 Mobile Share Receiver Fallback And Hosted Custom-Scheme Proof

  • Change: add a mobile public-share preview repository/hook backed by get_public_share_preview, then make /s/[sessionId] and /m/[matchId] render useful public previews when the private member-only read is blocked by RLS. Stabilize the generated R1-3 /lb hosted smoke assertion from the below-the-fold 소식 tab to the first-viewport public-club 가입하기 CTA.
  • Classification: OTA-safe mobile JS/TS behavior plus test/evidence tooling. No native config, app runtime version, database, or edge-function change.
  • App source commit: 111dbf77eb590b2ee6a49b0c70bffc3c2a5a342b (fix(mobile): fallback public share receivers).
  • Test-tooling source commit: 55a901264df8c7a4780ee2953c385bc72609ee80 (test(mobile): stabilize r1 share deeplink smoke).
  • Source sync: app source was upstream-synced before OTA; tooling/docs sync is part of this handoff.
  • Source sync proof: yarn ota:preview guard passed with clean tree and 111dbf77 synced with origin/main.
  • Target: preview OTA plus hosted Android Maestro rerun.
  • App version: 0.6.1.
  • Runtime version: 0.6.1.
  • EAS artifact: update group e01fc19c-19a8-4012-8ec8-1555edad14d6; Android update 019f0c2b-2fae-7354-9ef0-fcd290f57b9a; iOS update 019f0c2b-2fae-763f-8729-dfe8098aa2df.
  • Compatible preview build: Android build 6aa365f6-7814-4250-a708-a27843a919dc, channel preview, runtime 0.6.1, completed 2026-06-27T20:37:57.922Z.
  • Backend artifact: none; reuses preview migration 00306_public_share_previews.sql and the deployed seed-scenarioclub_growth share fixtures.
  • Pre-fix failure evidence: hosted workflow 019f0c17-0e66-7ad8-b0b6-47c2eb9747ef failed on "성동구 정기전" is visible; failure screenshot showed 이 일정을 찾을 수 없어요. Root cause was the mobile /s receiver using private sessions_select membership-gated reads for a public share opened by a non-member preview dev account.
  • Post-OTA generator failure evidence: hosted workflow 019f0c2b-9027-7d85-b3c4-a66dc39e99ef failed on "소식" is visible after the app had already landed on the public club profile. Root cause was a fragile below-the-fold tab assertion in the generated flow, not a product routing failure.
  • Hosted pass evidence: hosted workflow 019f0c32-1afc-70c5-82d8-185df88e8ad4 passed generated r1-share-deep-links.yaml against the reused Android preview build. JUnit artifact 019f0c35-bbfc-7d0c-90f6-fa939bae691e reports tests=1, failures=0, time=38.0; Maestro results artifact 019f0c35-ba30-7022-b1e3-eb49653ef882 includes command traces for twomore://p/..., twomore://s/..., twomore://lb/..., and twomore://m/....
  • Verification: focused repository tests for public share preview adapter; yarn workspace @twomore/app typecheck; yarn workspace @twomore/mobile typecheck; full yarn check; preview OTA publish; node --check scripts/prepare-r1-share-deep-link-flow.mjs; git diff --check; hosted workflow 019f0c32-1afc-70c5-82d8-185df88e8ad4.
  • Smoke evidence: the hosted command trace asserted profile, session, club leaderboard, and match recipient-return surfaces: session and match showed 성동구 정기전 plus 성동 베어본 테니스; leaderboard showed 성동 베어본 테니스 plus 가입하기.
  • Monitoring checked: not applicable; pre-production preview OTA and hosted smoke.
  • Rollback pointer: previous preview OTA group on runtime 0.6.1 was 3ea7b802-e498-4197-a081-0284f3f49c12; revert app source commit 111dbf77 if the fallback behavior must roll back. Revert test-tooling commit 55a90126 if the generated proof assertion must return to the prior behavior.
  • Known gaps: R1-3 remains active for Kakao-native share template proof and true HTTP iOS Universal Links / Android App Links through the required native configuration, website association files, and preview build proof.
  • Source detail: R1 Activation Source Pack.

2026-06-28 - R1-3 Custom-Scheme Device-Smoke Tooling

  • Change: add scripts/prepare-r1-share-deep-link-flow.mjs, root prepare:r1-share-deep-link-flow / e2e:r1-share-deep-links scripts, ignored Maestro generated-output path, and targeted EAS workflow maestro-r1-share-deep-links-rerun.yml.
  • Classification: test/evidence tooling only. No native config, runtime, database, edge-function, or mobile bundle behavior change.
  • Source commit: de703f539076523f7884c79189e966862c730330.
  • Source sync: pushed with this evidence handoff.
  • Source sync proof: final handoff push from main; no OTA guard was run because no OTA/build artifact was published.
  • Target: generated local/hosted Maestro flow; no preview OTA/build.
  • App version: 0.6.1 unchanged.
  • Runtime version: 0.6.1 unchanged.
  • EAS artifact: none; no hosted Maestro run yet.
  • Backend artifact: none; generator invokes already deployed club_growth.
  • Verification: node --check scripts/prepare-r1-share-deep-link-flow.mjs; git diff --check; yarn prepare:r1-share-deep-link-flow.
  • Smoke evidence: generator passed and wrote apps/mobile/.maestro/generated/r1-share-deep-links.yaml with current preview IDs for profile d38632ed-69d3-4557-bc0b-03b54a460cf5, session 59ffa609-b522-4293-9dc7-4d0686887102, club 5a762dcf-081a-48cf-8aba-822dd4d56dd4, and match 67c7dd68-7567-4b10-ac3c-bdc1bebedd7e.
  • Monitoring checked: not applicable; no runtime artifact.
  • Rollback pointer: revert source commit de703f53; generated YAML is ignored and can be deleted locally.
  • Known gaps at this checkpoint: local Maestro CLI was not installed in this environment, so maestro test apps/mobile/.maestro/generated/r1-share-deep-links.yaml did not run. Hosted proof was later closed by R1-3 Mobile Share Receiver Fallback And Hosted Custom-Scheme Proof.
  • Source detail: R1 Activation Source Pack.

2026-06-28 - R1-3 Public Share Receiver Smoke

  • Change: add bounded public share-preview RPC, move /s, /m, /p, and /lb web receiver pages plus OG images onto that RPC, expose server-rendered twomore:// fallback links, extend club_growth with deterministic share fixture IDs, and add yarn smoke:r1-share-receivers.
  • Classification: Supabase migration plus edge-function deploy plus web route behavior/tooling. No native config, runtime, or mobile bundle behavior change.
  • Source commit: ce6a23185a322c51c2d31f301b0a82fc96f82bba.
  • Source sync: pushed with this evidence handoff; no OTA guard was run because no OTA/build artifact was published.
  • Source sync proof: final handoff push from main; OTA source guard not applicable because the target is backend deploy plus web/local smoke.
  • Target: backend deploy plus local web smoke; no preview OTA/build.
  • App version: 0.6.1 unchanged.
  • Runtime version: 0.6.1 unchanged.
  • EAS artifact: none; no mobile OTA/build run.
  • Backend artifact: yarn supabase db push --linked applied 00306_public_share_previews.sql to the linked preview project; yarn supabase functions deploy seed-scenario redeployed seed-scenario to project llhvhhpbjnnrfwcczizc.
  • Artifact-order note: the preview DB push and edge-function deploy happened during implementation before source commit ce6a2318 existed. The source is now committed and pushed with this ledger; do not treat this as the preferred order for future artifacts.
  • Verification: yarn workspace @twomore/web typecheck; yarn workspace @twomore/app typecheck; yarn lint:pi-schema; git diff --check; yarn smoke:r1-share-receivers --seed --base-url=http://127.0.0.1:3100; yarn workspace @twomore/web build.
  • Smoke evidence: seeded receiver smoke passed for /s/59ffa609-b522-4293-9dc7-4d0686887102, /m/67c7dd68-7567-4b10-ac3c-bdc1bebedd7e, /p/d38632ed-69d3-4557-bc0b-03b54a460cf5, /lb/5a762dcf-081a-48cf-8aba-822dd4d56dd4, each route's /opengraph-image, and /join-club?code=JOINR12.
  • Web build evidence: production web build passed and produced dynamic receiver routes for /s/[sessionId], /m/[matchId], /p/[userId], and /lb/[clubId]. It emitted the existing non-blocking expo-location export warning from use-user-location.ts, unrelated to this slice.
  • Monitoring checked: not applicable; no production artifact.
  • Rollback pointer: revert web/source commit for receiver behavior, deploy the previous seed-scenario source if fixture output must roll back, and apply a follow-up migration to revoke/drop get_public_share_preview if the RPC must be disabled. No mobile update group was created.
  • Known gaps at this checkpoint: R1-3 remained active for Kakao-native share templates, iOS Associated Domains, Android App Links, website association files, preview-build proof for native app-link config, and installed-preview/device return smoke. Installed-preview custom-scheme smoke was later closed by R1-3 Mobile Share Receiver Fallback And Hosted Custom-Scheme Proof.
  • Source detail: R1 Activation Source Pack.
  • Change: make public share URLs canonical in route helpers, generate share-card URLs through those helpers, and change club invite shares to /join-club?code=<inviteCode> with mobile/web prefilled join-code support.
  • Classification: OTA-safe JS/TS client behavior plus web route-shell behavior; no native config, runtime, database, or edge-function change.
  • Source commit: bf3c8746c18401e95378eca40ab3345a180ff890.
  • Source sync: upstream-synced.
  • Source sync proof: yarn ota:preview guard passed with clean tree and bf3c8746 synced with origin/main.
  • Target: preview OTA.
  • App version: 0.6.1.
  • Runtime version: 0.6.1.
  • EAS artifact: update group 3ea7b802-e498-4197-a081-0284f3f49c12; Android update 019f0bd5-d1c4-71eb-96b3-813f3e227785; iOS update 019f0bd5-d1c4-7612-938e-00fe4198e90c.
  • Backend artifact: none.
  • Verification: yarn workspace @twomore/app test src/navigation/routes.test.ts src/presentation/utils/__tests__/share-cards.test.ts --runInBand; yarn workspace @twomore/clubs test src/shared/__tests__/join-club-utils.test.ts --runInBand; yarn workspace @twomore/clubs typecheck; yarn workspace @twomore/app typecheck; yarn workspace @twomore/web typecheck; yarn workspace @twomore/web build; yarn check; pre-push hook.
  • Smoke evidence: no hosted device smoke for invite-link opening yet. Web production build passed and produced /join-club, /s/[sessionId], /m/[matchId], /p/[userId], and /lb/[clubId] routes. The web build emitted the existing non-blocking expo-location export warning from use-user-location.ts, unrelated to this slice.
  • Monitoring checked: not applicable; pre-production source slice.
  • Rollback pointer: previous preview update group on runtime 0.6.1 (ff478b2e-4058-49ce-8d0d-333b66eabaa9); production OTA remains blocked until R4.
  • Known gaps at this checkpoint: R1-3 remained active for seeded receiver smoke, Kakao-native share templates, iOS Associated Domains, Android App Links, website association files, and preview-build proof for native app-link config. Seeded receiver smoke was later closed by the R1-3 Public Share Receiver Smoke entry above.
  • Source detail: R1 Activation Source Pack.

2026-06-28 - R1-2 Create-Club First-Session Smoke

  • Change: add a created-owner first-session shortcut beside the empty upcoming-schedule section and retarget the R1-2 create-club smoke to that product affordance.
  • Classification: OTA-safe JS/TS client behavior plus Maestro flow update.
  • Source commit: a5d2a9135ed8a5f5dd3cf11ab6cc8cdbe95f3b34.
  • Source sync: upstream-synced.
  • Source sync proof: yarn ota:preview guard passed with clean tree and a5d2a913 synced with origin/main.
  • Target: preview OTA.
  • App version: 0.6.1.
  • Runtime version: 0.6.1.
  • EAS artifact: update group ff478b2e-4058-49ce-8d0d-333b66eabaa9; Android update 019f0bb0-b9bc-7c87-8581-d6aa951247a3; iOS update 019f0bb0-b9bc-755f-beee-8a96b9f4e137.
  • Backend artifact: none; reused deployed r1_create_club_zero seed scenario.
  • Verification: yarn workspace @twomore/clubs typecheck; yarn lint:strict; YAML parse of 10-club-create-first-session-cta.yaml; yarn check; node scripts/run-scenario.mjs r1_create_club_zero; pre-push hook.
  • Smoke evidence: targeted hosted workflow 019f0bb1-2f5f-7845-bc8b-3d8ab65bbdfe reused Android preview build 6aa365f6-7814-4250-a708-a27843a919dc, seeded r1_create_club_zero, and passed 10-club-create-first-session-cta.yaml in 1m 11s. Artifacts: JUnit report 019f0bb5-212d-7c29-ae01-2c7b796d8360; Maestro results 019f0bb5-1f40-730a-9b31-5c27170d5fd5.
  • Monitoring checked: not applicable; pre-production targeted smoke.
  • Rollback pointer: previous preview update group on runtime 0.6.1 (28950118-aeca-42a3-8de1-ec3e9df218e6); production OTA remains blocked until R4.
  • Known gaps at that checkpoint: R1-3 share/deep-link activation and R1-4 club-growth evidence packet remained pending; mobile preflight was later closed by source commit daf2a01d6244578a9d5af3eef4000bfd133556ac.
  • Source detail: Rebuild Log.

2026-06-28 - R1-2 Approval Join Smoke Stabilization

  • Change: stabilize approval join funnel and hosted smoke flow.
  • Classification: OTA-safe JS/TS client behavior plus Maestro flow updates.
  • Source commit: 3645791c46dc3d61a0e0cd4ba7074d4169358d94.
  • Pushed: yes.
  • Target: preview OTA.
  • App version: 0.6.1.
  • Runtime version: 0.6.1.
  • EAS artifact: update group 28950118-aeca-42a3-8de1-ec3e9df218e6; Android update 019f0b2a-3bea-7206-975e-23e714b8898b; iOS update 019f0b2a-3bea-7435-9c8a-652a36909005.
  • Hosted smoke: rerun 019f0b37-4a40-7eff-8727-d31d22a43a43 passed enabled flows 01-login, 02-main-tabs-smoke, 08-club-growth-media, and 09-club-join-funnels using Android preview build 6aa365f6-7814-4250-a708-a27843a919dc.
  • Flow-only follow-up: commit 7c0bd42756e424f55518d58a874a0c34093a9d7d; pushed; no additional OTA required.
  • Rollback pointer: previous known-good preview update group on runtime 0.6.1; production OTA remains blocked until R4.
  • Known gaps: R1-2 still needs create-club -> first-session CTA proof.
  • Source detail: Rebuild Log.

2026-06-28 - R1-2 Join Smoke Stabilization

  • Change: stabilize join RPCs, fixture seed idempotency, and R1-2 join funnels.
  • Classification: Supabase migration plus edge function deploy plus OTA-safe JS/TS client behavior.
  • Source commit: 93de2c86e5ed1de45bced234ee5bd24fe6c89760.
  • Pushed: yes.
  • Target: preview OTA.
  • App version: 0.6.1.
  • Runtime version: 0.6.1.
  • Backend artifacts: migration 00305_signal_immutability_invoker_boundary.sql applied to preview; seed-scenario edge function deployed.
  • EAS artifact: update group 1ac260bd-fe8e-49e3-924d-b7ce4619217a; Android update 019f0b09-7081-7b87-8bfd-5bcfd00ce38a; iOS update 019f0b09-7081-7b8f-b27e-5cebd9a94c62.
  • Hosted smoke: follow-up hosted runs improved the path but still failed before the later approval join stabilization.
  • Rollback pointer: previous preview update group on runtime 0.6.1; production OTA remains blocked until R4.
  • Known gaps at that point: hosted 09-club-join-funnels.yaml still red after the first OTA; later closed by the approval join stabilization entry above.
  • Source detail: Rebuild Log.

Entry Template

md
### YYYY-MM-DD - <Release Or Attempt Name>

- Change:
- Classification: docs-only | OTA-safe | preview-build | backend-deploy | production-hotfix | failed-attempt
- Source commit:
- Source sync: upstream-synced | not applicable
- Source sync proof: `node scripts/guard-ota-release.mjs <target>` passed | `git log --oneline @{u}..HEAD` empty | not applicable
- Target: preview OTA | preview build | backend deploy | production canary | production full | no artifact
- App version:
- Runtime version:
- EAS artifact:
- Backend artifact:
- Verification:
- Smoke evidence:
- Monitoring checked:
- Rollback pointer:
- Known gaps:
- Source detail:

Update Procedure

  1. Classify the change with Release Versioning And OTA Workflow.
  2. Commit and push the source before a significant OTA/build artifact is claimed.
  3. Run the artifact command only from the committed source state.
  4. Add or update one evidence entry here after the artifact attempt.
  5. If the artifact affects a roadmap slice, also update the active source pack or implementation slice.
  6. Run:
    bash
    yarn docs:generate
    yarn docs:check
    git diff --check

docs/rebuild-log.md remains useful chronology. This page is the shorter artifact ledger that future agents should scan first.

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