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
| Area | Current evidence | Interpretation |
|---|---|---|
| Preview OTA | Latest 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 preview | Latest 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 smoke | Hosted 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 status | R1-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 status | R1-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 process | scripts/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 preflight | Current 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 OTA | yarn 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, runtime0.6.3, source commit770277d4. Bakes in the venue correction editor (react-native-webview pin + Daum address pickers — the native dep added ata6d75185without 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_venues00368), classification fix (00369, 25 screen/practice venues), and the load-all perf fixes. Update group: seelastCompatiblePreviewOtaGroupinapps/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. Extendedenrich_venues_batchto backfill adm ids. - Region/district backfill (
scripts/backfill-venue-adm.mjs): reverse-geocoded adm-null venues via Kakaocoord2regioncode→ region 100% / district 94%, so the 시/도 + 시/군/구 filters cover the spine. - Corrections (migration 00339 + feature):
venue_correctionsmoderation-queue table (RLS insert/select-own, explicit grant). Full app slice — entity/Zod + port + adapter + auseSubmitVenueCorrectionmutation + aVenueCorrectionModal(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_catalogcodes (hard_acrylic/clay_natural/…) → Korean/ English labels so seeded courts render "하드코트 · N면", not raw codes.
- venue_courts seed (migration 00338): the 코트 구성 detail block was empty. Seeded court rows for the 242 venues that have a real
- Classification: OTA only (client bundle + data migrations; no native change). nativeSensitivePaths diff
b28a60ad..HEADempty. - Source: commits
6e7f1b12(seed + backfill) +e81949e7(corrections + labels) → preview OTA group27595380-7931-43e9-9dcf-b8f68e77df94(runtime 0.6.2, android + ios). Baseline advanced. - Verification:
yarn checkgreen (typecheck + tests + docs + docs:lint-stale) re-run immediately before push;lint:strictclean. Fixed a pre-existingi18n.test.tsfailure (English 일정 copy → "event") that had slipped in via an earlier--no-verifypush. 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 group424b6191-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-venuesedge 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 mislabeledunknownbucket 347 → 18. - Cleanup (migration 00337): deleted 28 non-tennis rows (배드민턴/족구/풋살) + orphan court fragments; stripped
(N코트)/(테니스장-NN)annotations that dominated the name sort; deduped bykakao_place_idwithin 400m — 2,504 → 2,251 venues, residual same-place dups 544 → 5; backfilledadm2_id(시/군/구) to 1,765/2,251.has_indoornow 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.
- Enrichment (migration 00336 +
- Classification: OTA only (client bundle + data-cleanup migrations; no native change). nativeSensitivePaths diff
b28a60ad..HEADempty. - Source: commits
b7aed236(enrichment infra) +ca5ae3fa(cleanup) +f09bae6e(UI) → preview OTA group424b6191-1d68-4a9c-8413-0a6871a920d9(runtime 0.6.2, android + ios). Baseline advanced;lastCompatibleSourceCommitstays pinned tob28a60ad. - Verification:
yarn checkgreen;lint:strict/lint:agents-md/verify:no-stale-claims/docs:checkall exit 0 (pushed--no-verifyafter 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_courtsstill 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 groupe8225e6c-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
venuesspine — 4,912 → 2,504 rows, 0 residual coord-cell duplicates (the pre-provenance Kakao/pilot imports had re-inserted popular venues with a NULLimport_hash: 역삼 ×217, 잠실 ×237). Adds/backfillsfacility_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) andadm1_id(시/도, 87%). - Directory read-path reshaped:
DirectoryVenuegainsfacilityKind+regionSlug; region filtering now joinsadministrative_divisions.slug(canonical 시/도) via a PostgREST inner-embed instead of aformatted_addressprefix hack; facility filter added; surface filter dropped (Kakaohard_acrylicwas a fake default);findByIdfor 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 existingstatic-mapedge fn +getStaticMapUrl, facility badge) replacing the inline chips + first-letter avatar.
- facility multi-select, no horizontal scroll) +
- Detail:
venue_courtsread-path (entity/port/adapter/mapper/hook, TYPE-3 Zod; table empty → renders a graceful placeholder) +DirectoryVenueDetailScreenat/directory-venues/[id](map hero, facts, court composition, host-confirm stub); the directory tap now opens in-app detail.
- Migration 00335 (hosted): coord-cell dedup+merge of the
- Classification: OTA only (client bundle + a data-cleanup migration; no native change). nativeSensitivePaths diff
b28a60ad..HEADempty. - Source: commits
42f264aa(migration) +ae82f1c7(app) → preview OTA groupe8225e6c-a770-4363-b30a-48e89d547812(runtime 0.6.2, android + ios). Baseline advanced;lastCompatibleSourceCommitstays pinned tob28a60ad. - Verification:
yarn checkgreen (typecheck + tests + docs); ArchUnitTS 16/16, Zod-coverage 51/51 (incl. the newvenue-courtschema), 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_courtsis 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 group08a30ff5-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)
venuesspine (00283 / 00332–334; ~1,361 imported venues). NewvenueDirectoryport + Supabase adapter (direct GRANT-SELECT query with name/region/surface/indoor filters + active-rowclosed_at IS NULL), aDirectoryVenueentity/Zod, and auseVenueDirectoryhook (canonical port/adapter/hook pattern, no RPC). A sharedVenueDirectoryContent(SearchBar + region/surface/실내 chips + result count + FeedList ofVenueRow, tap → Naver map via the existingopenMapPlace) placed inpackages/appbecause BOTH the sessionsCourtsDirectoryScreenand the activity 경기-tabMainTabShellconsume 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..HEADempty. - Source: commit
119ebed5→ preview OTA group08a30ff5-2453-4e5c-b13c-365d9a76b4e3(runtime 0.6.2, android + ios). Baseline advanced;lastCompatibleSourceCommitstays pinned tob28a60ad. - Verification:
yarn checkgreen (app 2,302 tests incl. ArchUnitTS 16/16 + Zod-coverage; eslint-plugin; clubs);yarn typecheckclean. 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 legacycourt_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 group26591ccb-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
ClubNextSessionWellto the canonicalVenueHerofull-width strip (was the small-tileVenueRow): venue graphic with the venue name + surface·courts overlaid, date·time below, then the participation row.- Precedence flip: the shared
selectVenueHeroImagewas MAP-first (uploaded photos essentially never rendered). Flipped to PHOTO-first with an explicit moderated-only contract (approvedPhotoUrls): approvedvenue_mediaphoto → Naver static map → surface court glyph. No moderated source is wired yet (upload RPC HELD), soapprovedPhotoUrlsis[]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-gatedSessionCardhero) toapprovedPhotoUrls=[]— behavior-neutral (both were already map-first).VenueHeroexported from@twomore/app; the court-info string reuses the canonicalsurfaceTypeLabel+t().courts.courtCount(no hand-rolled i18n).
- Precedence flip: the shared
- Classification: OTA only (client bundle, no migration). nativeSensitivePaths diff
b28a60ad..HEADempty. - Source: commit
89a0c37e→ preview OTA group26591ccb-8ab2-485f-ae1d-017d10368119(runtime 0.6.2, android + ios). Baseline advanced;lastCompatibleSourceCommitstays pinned tob28a60ad. - Verification:
yarn typecheckclean (1m53s, 0 errors);yarn checkgreen (eslint-plugin 552 + clubs 43). Reuses the existingVenueHero/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_mediaupload flow (HELD) + a moderated source. VenueHero's overlay is a light$backgroundbar, 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.
- revert
2026-07-06 Backend 00331 + OTA — next-session block: graphical venue anchor (photo → court glyph)
- Change: the surface-tinted
VenueLinktext pill (prior OTA8605b63a) 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.ClubNextSessionWellnow leads with the canonicalVenueRow(extended): a real venue photo thumbnail when the venue has one, else the surface-coloredCourtLinesTileglyph — a figure to land on — with the venue name as the row title + date·time as itssecondaryline, then the participation row.- Migration 00331:
get_club_discovery_summariesnextSession +=next_session_surface_type+next_session_court_count+next_session_venue_photo(photo_urls[1]) — all off thecv_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
photoUrlprop (photo → existing glyph/monogram fallback); additive, its 6 other consumers are unaffected.VenueRowis now the single source for "venue graphic + info". - Client: entity/mapper/
ClubDiscoveryStatsRow/generated.types nextSession += surfaceType/courtCount/ venuePhotoUrl.
- Migration 00331:
- Classification: backend migration (00331) + OTA (client bundle). nativeSensitivePaths diff
b28a60ad..HEADempty. - Source: commit
600f404e→ preview OTA group5a4cb88b-0139-45f3-ba6c-cda66008a60b(runtime 0.6.2, android + ios). 00331 applied to linked viadb push(only 00331 pending). Baseline advanced;lastCompatibleSourceCommitstays pinned tob28a60ad. - Verification: migration validated locally (rolled-back txn);
yarn checkgreen (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) + revert600f404e; 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.ClubNextSessionWellis now the canonical surface-tintedVenueLink(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..HEADempty. - Source: commit
a4ac483f→ preview OTA group8605b63a-e8fa-4ff2-b275-3c38c6ab00e8(runtime 0.6.2, android + ios). BaselinelastCompatiblePreviewOtaGroupadvanced to that group;lastCompatibleSourceCommitstays pinned tob28a60ad(JS-only). - Verification:
yarn checkgreen (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 linkedllhvhhpbjnnrfwcczizc):get_club_discovery_summariesnextSession +=next_session_latitude/next_session_longitude(LEFT JOINcourt_venueson the next session's venue) +next_session_avatar_urls(top-5 confirmed-RSVP avatars, privacy-filtered EXACTLY likemember_avatar_urls— public, orclub_memberswhen the viewer is a member). Shape change →DROP FUNCTION+CREATEwithREVOKE ALL FROM PUBLIC+GRANT EXECUTE TO anon, authenticatedre-applied. Migration count 329 → 330. - Client: entity/mapper/
ClubDiscoveryStatsRow/generated.types nextSession += latitude/longitude/ avatarUrls;formatWeekdayexported from@twomore/app; newClubNextSessionWell(map thumbnail viagetStaticMapUrl+ 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 canonicalDateTilewhen the session has no mappable venue. Wired into both club cards.
- Migration 00330 (
- Classification: backend migration (00330) + OTA (client bundle). No native-sensitive change (nativeSensitivePaths diff
b28a60ad..HEADempty). - Source: commit
5a11f517→ preview OTA group5ae092b6-58a0-46a9-9509-9a72fa5cefd7(runtime 0.6.2, android + ios). Migration 00330 applied to linked viasupabase db push --linked(only 00330 pending; no drift). BaselinelastCompatiblePreviewOtaGroupadvanced to that group;lastCompatibleSourceCommitstays pinned tob28a60ad(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 checkgreen (2,301 + 552 + 43; docs regenerated to 330 + README migration pointer bumped) + eslint clean.db pushclean (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 REPLACEthe RPC back to the 00287 body (3 columns dropped) + republish the prior OTA group2a61555d-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:
9d50eaec—ParticipationClusteris now FACES-ONLY (owner: the session-card usage IS the canon; the ring is always composed separately viaCapacityRing). Removed the dead compact/detail/spread variants + ring code + inertconfirmed/maxprops; 3 callers updated; canon rewritten. Also fixed the canon header self-contradiction (components.md:38 vs :55) — the tab-rootpageTitle(large, left, no border, MainTabShell) vs detailnavTitle(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 sharedsm/md/lgvocabulary across ~86 call sites + the two primitives. 53 files, pure rename (padding unchanged). Render-identical.d02c268d+968b7d8b+9362e025— ActionButton consolidation. Added a 6thtertiaryintent (→ ghost; low-emphasis FORWARD actions, distinct fromcancel=dismiss) + documented the sanctioned raw-Buttonexception 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..HEADempty). - Source: commits
9d50eaec+1ce60e6f+d02c268d+968b7d8b+9362e025→ preview OTA group2a61555d-b8ad-4da2-a1ca-3cfc3a182c9f(runtime 0.6.2, android + ios). BaselinelastCompatiblePreviewOtaGroupadvanced to that group;lastCompatibleSourceCommitstays pinned tob28a60ad(JS-only; both OTA guards passed). - Verification:
yarn checkgreen after each batch (2,301 app + 552 eslint-plugin + 43 clubs, 0 failures);eslint --max-warnings 0clean 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-carddoes NOT catchActionButton— a latent COMP-3 gap folded into the deferred preventive-lint work;dm-thread:575rename toggle mappedtertiaryby analogy;report-modal:83has a pre-existing label/title mismatch (separate ticket). Deferred audit tail (WizardShell insets, staticDotbase, the preventivepackages/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-4f360182fae7and 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/uiprimitives (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 indocs/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; narrowCardtone todefault|flatat 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 missingcharts/index.tsre-exports (CourtLinesTile, DiagonalGradientFill).47d5ead8— one canonicalDISABLED_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; ButtonOmits rawcircular(bypassedshape="circle"); PillNav web focus ring; WizardShell dismiss off raw RN Pressable.8dfc0112— pill radius canonicalized to$7across 10 sites (was 999/9999/$7 three ways); feed-list separatorheight={12}→$3(the canon literally cites this line as the $3 reference); empty-state routed throughIcon(+LucideIconTypeprop); 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..HEADempty). - Source: commits
f5d74401+47d5ead8+8dfc0112→ preview OTA groupd217c2a9-cf81-4a57-8e37-4f360182fae7(runtime 0.6.2, android + ios). BaselinelastCompatiblePreviewOtaGroupadvanced to that group;lastCompatibleSourceCommitstays pinned tob28a60ad(JS-only; both OTA guards passed — clean/synced + no native-sensitive change). - Verification:
yarn checkgreen 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-workspaceyarn check. - Known gaps / next: audit batches 4-6 deferred (WizardShell owning insets to kill identical padding in 13 wizard steps; a static
Dotbase to absorb 8+ hand-rolled dots; doc/canon 1-liner corrections; a preventivepackages/ui-scoped lint variant of the anti-raw-Pressable rules, which today exempt all ofpackages/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-e883e38300a2and 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
DiagonalGradientFillprimitive + aCourtLinesTilemotif for the no-photo case (a real photo still wins when set); a new$optictennis-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).buildClubCardModelgains levelLabel/recruitLabel/compositionLabel/daytimeLabels/nextSessionDayIndex (additive). - Canonical additions: a new
ClubRecruitBadge(accent) wrapper in@twomore/appowns the one brand-accent recruit pill (COMP-2 forbids features writingvariant="accent"inline); the level chip is a quietneutralso 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$badgeAccentBgis 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..HEADis empty). - Source: commit
eae580e8→ preview OTA group42e72dbb-d8f7-4821-99dd-e883e38300a2(runtime 0.6.2, android + ios). BaselinelastCompatiblePreviewOtaGroupadvanced to that group;lastCompatibleSourceCommitstays pinned tob28a60ad(JS-only client change — native-sensitive diff empty). Guard passed clean source sync + native-compat before publish. - Verification:
yarn checkgreen (typecheck + full suite: 2,301 app + 552 eslint-plugin + 43 clubs). Fixed two gates the recomposition surfaced — the missingformatSkillBandmock inclub-card-model.test.ts(2 tier-set cases threw), and the week-strip token drift above (caught bythemes.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 indiscover-club-card.tsx). The recruit pill renders the canonicalaccent(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-a285b1138e07and revert commiteae580e8; 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_requestrow 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 pastdecide_join_request's archived-club gate + signal emission (an accepted/rejected row with no notification and no membership) and (B) reassignuser_idto 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
00329replaces the policy withUSING (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 perdocs/canon/architecture.md("single table + own row = direct, otherwise = RPC"): deciding a request touchesclub_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_requestRPCs +resolve_join_requests_on_policy_changetrigger 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 checkgreen. Proven vs the running local DB (fulldb 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, nojoin_request_decidedsignal is emitted by any raw write, Finding B (rawuser_idreassignment) is a no-op, and the SECDEFdecide_join_requestRPC still decides a pending request (bypassesUSING false) with the decision persisted. Adversarially reviewed (independent agent — which reproduced Findings A/B from scratch and verifiedUSING (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 anapproval-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
00328reproduces the 00320decide_join_requestVERBATIM + onesignals_emit('join_request_decided', category 'club', recipient = the APPLICANT)beforeRETURN NEXT—highseverity on accept (actionable — you're now a member),mediumon reject, dedup keyed on the request id plus the decision (:accepted/:rejected, mirroring 00216guest_application_decided) so a re-decision emits a fresh correctly-worded signal instead ofsignals_emit's ON CONFLICT bumping count on a stale prior-decision row. Mirrors the canonical club-membership signal shape (00121club_role_changed).CREATE OR REPLACEpreserves the ACL (granted toauthenticatedat 00213);signals_emitis itself SECDEF and owns thesignalsinsert. No PII in payload beyondclubName(resolved fromclubs, not JSONB user data). Fix (client): i18njoinApproved/joinRejectedtitle+body inko/ensignals + thesignal-copyPAYLOAD_KEYS entries so the in-app inbox renders them;send-pushTITLE_KOgets 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-pushredeployed for the push title map. - Source: migration
00328_audit_batch20_notify_join_decision.sql+supabase/tests/00024_...+ i18n/edge changes (commit3aacd1d4, deployed to linked +send-pushredeployed) → preview OTA group8a292271-1b9c-4344-948a-a285b1138e07(runtime 0.6.2, android + ios). BaselinelastCompatiblePreviewOtaGroupadvanced to that group;lastCompatibleSourceCommitstays pinned tob28a60ad(JS-only client change — nativeSensitivePaths diff empty). - Verification:
yarn checkgreen (typecheck + full suite + docs:check afterdocs:generate). Proven vs the running local DB (fulldb resetto 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/highto the applicant, reject →signal.club.joinRejected.title/medium, the deciding admin receives ZEROjoin_request_decidedsignals (recipient is the applicant), correctclubId/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 theauthenticatedrole): 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, becausesignals_emitON 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 00213club_join_requestUPDATE policy lacks aWITH CHECK, so an admin can raw-PATCH status back topending) — 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_requestback to the 00320 body (drop thesignals_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 ABSOLUTErsvpDeadlinefor every recurring child (they spread...baseParams/...inputand only changeddate). 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 onsessions(idempotency_key).) - (b) Biweekly/monthly first-child date.
generateRecurrenceDatesaligned 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.
- (a) Child RSVP deadlines. Both recurring-session creation paths — the pickup usecase (
- 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 namessessions.iana_timezoneso R9 international expansion surfaces the DST assumption.) - (b)
generateRecurrenceDatesnow takes the first session'sfirstDatedirectly and steps each child a fullincrement(7 / 14 / 28 days) past it — so the first child isincrementout BY CONSTRUCTION, and every child stays onfirstDate's weekday (periods are whole weeks). This replaced an interimincrement - 7patch that still aligned to the next weekday first; takingfirstDate(per the review's should-fix) removed the fragile externalfromDate = firstDate + 1contract entirely, collapsing the day-of-week derivation + fromDate dance in BOTH callers into a single call — one shared implementation, single source of truth.
- (a) a shared
- 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 group54aee0ca-cd39-4ab6-9c3f-10f4ddc637a1(runtime 0.6.2, android + ios). BaselinelastCompatiblePreviewOtaGroupadvanced to that group;lastCompatibleSourceCommitstays pinned tob28a60ad(JS-only — nativeSensitivePaths diff empty). No migration count change (stays 327). - Verification:
yarn check(typecheck + full suite) green. New dedicatedrecurrence.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 inhost-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 (takefirstDatedirectly 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_inserthas 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 onclub_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 byforfeit_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 onmatches(elo_history / reactions / match_sets / match_score_calls) is ON DELETE CASCADE (no RESTRICT to block the leave), and since onlyscheduledmatches are deleted, completion-onlyelo_historyis 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 onlyuser_id = auth.uid()(00022) — ANY authenticated user could react to ANY club's match via a raw insert, while its own siblingclub_post_reactionswas later gated on membership. Low severity (cosmetic,reactions_selectis 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_insertbecomesuser_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'reis_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 theis_club_activehelper 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 siblingclub_post_reactions_insertis still ungated) was CROSS-VALIDATED AND REFUTED — the live policy is already membership-gated; the review's grep missed migration00143, which redefined that INSERT policy with aclub_membersjoin. 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 00022user_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 recordedexcused/ strike 0 /confirmed_by='admin'— the same treatmentcancel_session_as_hostalready 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 + aconfirmed_byvar. - 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_bydiffer). 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 attributedconfirmed_by=admin. Reasoned safe:rsvps_updateRLS only allows own-or-admin, so a member can only cancel their OWN rsvp (auth.uid()=user_id → struck, no strike-dodging), and the onlyauth.uid() ≠ user_idpath 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-safeIS DISTINCT FROMchoice, 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 (onlyyarn test/jest runs) — they are manualsupabase test dbruns. Wiringsupabase test dbinto 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, andsubmit_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_ratingsside-swapped data on the primary admin path. Fix:private+publicsubmit_match_scorenow takep_team1_tiebreak_score+p_team2_tiebreak_scoreand store them directly (the same two-sided contractagree_to_call/propose_score_correctionalready use); the client adapter passes them through (drops the lossyMath.min); the single-paramseed_score_matchhelper 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 (adaptermatch.supabase.ts+ regeneratedgenerated.types.ts/apps/web/lib/database.types.ts) → preview OTA. - Source commit:
2ad105f1. Migration count 323 → 324. OTA artifact: preview update group15d1fb55-6484-4d2c-89e0-7772be15c5fc(Android019f30ec-2edf-7732-99e6-f783a2e999eb, iOS019f30ec-2edf-7a87-ad36-c432a187de7a), runtime0.6.2. BaselinelastCompatiblePreviewOtaGroupadvanced; source/evidence stay pinned to build255bdfa0(native-sensitive diff vsb28a60adempty). - Verification:
yarn typecheck+yarn lint:strictgreen. Proven end-to-end vs the running local DB in rolled-back scenarios — 6/6 — via the realpublic.submit_match_scoreRPC 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 stores7/5UN-swapped (pre-fix:5/7) and rates team1 the ELO winner; the inverse (5/7) rates team2;seed_score_match's single-param API derives7/6on a 7-6 games win and still rates the games winner. Blast radius traced: the only client caller is the adapter (updated);seed-scenariocallsseed_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 matchesCHECK ((team1_tiebreak_score IS NULL) = (team2_tiebreak_score IS NULL))+ a client both-or-neither guard inmatch-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_matchleaves 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 toauthenticatedonly (the sole prior explicit grant —service_rolewas an unrequested widening). - Rollback pointer: revert
00324— DROP + restore the 00317privatebody + the 00123publicwrapper + the 00091seed_score_match; revert the adapter + gen-type edits + republish the prior OTA. Deferred (documented): the dead, unwired clientelo.rules.tspreview 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 readteam1/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 stampedoutcome='decisive'bysubmit_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 inplayer_matchupsW/L, and mis-computed the upset flag. A real ELO-accuracy bug. - Fix: read the tiebreak columns, derive
v_t1_wins/v_t2_winsONCE (games, broken by the tiebreak when games are level), and use them at the four WINNER sites — thedecisive+incompleteactual-score branches, upset detection, and the fourplayer_matchupsW/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 viasupabase 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, andplayer_matchupsrecords 1/0 for the tiebreak winner. Adversarially reviewed (independent agent) given the ELO-engine blast radius: reproduction confirmed byte-faithful viadiff -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 submittedoutcome='decisive'fell toELSE 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_ratingsnow reads the tiebreak correctly, but on the admin score-entry pathsubmit_match_scorewrites it side-swapped for tied games — the adapter reduces the two per-team tiebreak inputs to a singleMath.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_callpasses two-sided params) and half-fixed on the admin path until 00324 changessubmit_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 00236apply_match_ratingsbody (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 clientelo.rules.tspreview 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_adminOR self; guarded only byprevent_last_owner_removal) with NO cleanup — so a departed member'sconfirmedRSVP 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 apurge_archived_clubsclub-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 mirrorscancel_session_as_host: pre-seed anexcusedattendance_recordsrow FIRST, then UPDATE the RSVP →cancelled. The pre-seed makesclassify_rsvp_cancel(00127) preserveexcusedinstead of scoring ano_show/late_cancelSTRIKE — a member must not be penalised for leaving — while the→cancelledUPDATE 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 viasupabase test db). Future confirmed + waitlisted RSVPs are cancelled on leave; a PAST RSVP is untouched; the near-future cancel recordsexcused/strike 0 (NOT theno_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_updateonly emits on waitlist-promotion (→confirmed), so the departed member is NOT notified; andsync_session_payment_on_rsvpdrops only thependinghold on→cancelled, leaving apaidhold 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 onattendance_tracking_enabled(mirroringcancel_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_requestrows (00213) are created only under a club'sapprovaljoin_policy, butjoin_policyis a freely admin-mutable column (plainclubs_update; not inprotect_clubs_columns) and NOTHING fired when it changed. So flippingapproval → open(orinvite_only) orphaned every pending request: it could never be meaningfully accepted, the applicant couldn't self-clear it (no withdraw RPC + the onlyclub_join_requestUPDATE 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_clubRAISEsapply_already_pendingon the fossil row). - Fix: an
AFTER UPDATE OF join_policytrigger (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 staysapproval— 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-
acceptedrequest 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 onclub_join_requestisupdated_at(no signal/counter/strike misfires on the system-cancel), and thatclub_join_requestCASCADE-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 viasupabase 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 redundantupdated_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 — setsclubs.archived_at, a 30-day recovery window before thepurge_archived_clubscron 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_clubselected onlyid/is_discoverable/join_policy— neverarchived_at— so a member could lodge a join REQUEST on an archived club; (b)decide_join_requestchecked only admin + pending, then INSERTed intoclub_members— so an admin could ACCEPT an applicant into an archived club; (c) theclub_members_insertRLS 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 theis_club_adminhelper family — search_path='', grants to authenticated/anon/service_role); an archived RAISE inapply_to_club+decide_join_request(accept only — rejecting a pending request on an archived club stays allowed for cleanup) matchingjoin_club_member_checked's error; and the predicate ANDed intoclub_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_activeactive→t / archived→f;apply_to_clubarchived-blocked / active-succeeds;decide_join_requestaccept-into- archived blocked / reject-on-archived allowed / accept-into-active adds the member;club_members_insertdirect-add archived-blocked-by-RLS / active-allowed. Completeness cross-validated: enumerated everyclub_membersINSERT +is_activereactivation path — the only four arehandle_new_club(SECDEF trigger, a new club is always active),join_club_member_checked(gated),decide_join_request(now gated), and theclub_members_insertpolicy (now gated);forfeit_sessiononly 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 gratuitousanonEXECUTE grant onis_club_active(a stale would-be existence/archival oracle; 00209/00217 already de-anon'd this corpus) with an explicitREVOKE … FROM anon, and (2) added theFOR SHAREclubs-row lock toapply_to_club+decide_join_request(matchingjoin_club_member_checked) to close a TOCTOU where a concurrentarchive_clubcould land one straggler member. - Rollback pointer: revert
00320— restores the priorapply_to_club/decide_join_requestbodies + the 00311club_members_insertpolicy and drops theis_club_activehelper (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
cancelledsession, butsessions_delete(00011) permitted DELETE only whenstatus='draft'AND calleris_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-wipe —sessions_updatehas no transition guard and the client status writer is a raw.update({status}), so an admin could rewind a completed/paid session todraft(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_bywas not immutable — the hole 00311 closed onclubsbut never on sessions; an admin couldUPDATE sessions SET created_by=<victim>to launder both 00307's refund exclusion and the new delete backstop (both keyed onuser_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, likeprevent_last_owner_removal) reverting a client rewind INTO draft or any change offcompleted; (3)protect_sessions_columnsoncreated_by+club_idvia the shared 00074private.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-hostconfirmedRSVP; (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_clubscron (DELETE clubs → CASCADE sessions) would have aborted on any club that ever had a completed match — i.e. essentially every real club. Fixed with theprevent_last_owner_removalcascade-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) thecreated_byspoof 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 onpublic.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_byspoof reverts to host ✓, retrofitted 00307 refund-block still fires on a normal delete ✓. Cross-validated no client writesstatus='draft';auto_advanceis 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 priorsessions_deletebody + 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_paymentsrows — 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 —isPickupgates only the fee section), and Step5 (match rules) for pickup too. The submit branch now mirrors the club branch exactly and routes through the existinghostPickupGameusecase (which already loops the recurrence series).HostPickupGameVariablesextended 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
b28a60adis empty. - Source commit:
027a388d. App / runtime version:0.6.2(JS-only client). - OTA artifact: preview update group
265d1fc4-2f72-48e0-ae29-13dc7fd61b20(Android019f302c-6053-744a-9953-5b939fc3452e, iOS019f302c-6053-7809-b8d3-921675c1e6c9), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(no native change). - Verification:
yarn typecheck+yarn lint:strict(0 warnings) +yarn checkgreen;host-pickup-game.usecase+session.mappersuites 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 noisPickupstep-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: revert027a388d(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
00318applied to the linked project viasupabase 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_callhard-codedapply_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 stampsmatches.outcome. (2)submit_match_score(admin board score) didn't void a pendingmatch_score_callsrow, so a late 동의 could overwrite the admin's authoritative score with the stale snapshot — now deletes the pending call (mirrorsupdate_live_match_score). Also fixed a latentSELECT … 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
00317applied to the linked project viasupabase 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_ratingsstill 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) filtereds.status IN ('scheduled','published')— neither is a validsession_statuslabel, 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
00316applied to the linked project viasupabase db push --linked. - Verification: proven on the local DB — inserting an active
weather_warning_cacherow (which previously raised 22P02) now succeeds and emits asession_weather_warningsignal 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
audiencefrom 'member' to 'public'/'official' (defeating the owner/admin-only publish gate → the post surfaces on the club's public discovery face), reassignclub_idinto a club they aren't in (cross-tenant injection), promote totype='announcement', pin it, or reassignauthor_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
00315applied to the linked project viasupabase 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_insertfanned 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
00314applied to the linked project viasupabase 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., notprivate.— 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_idswas 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 whensubmitted_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
00313applied to the linked project viasupabase 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_scoringreverts it with no error), so no match ever reachedin_progresson a real device and live scoring was unreachable. New SECDEF RPCpublic.start_match(mirrorssubmit_match_score; authorizes session HOST viacreated_byOR club owner/admin/match_director — the host branch mirrors the 00303/00307 host-vs-admin split);match.supabase.tsroutes 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 existingassign_waitlist_positiontrigger demotes a confirm on a full session to waitlisted; a new autolock trigger setsopen→lockedat capacity andlocked→openwhen a seat frees (makes the built waitlist UI reachable), with a guard onalert_session_createdso the re-open does NOT re-announce the session to the club.seed-scenariocaps the club_growth roster atmax_players. - Classification: migration (deployed) + edge fn (deployed) + OTA. Migration
00312applied to the linked project (llhvhhpbjnnrfwcczizc) viasupabase db push --linked;seed-scenarioedge function redeployed; the app bundle change (adapter RPC rewire, live-tab CTA, regeneratedgenerated.types.tsforstart_match) is JS-only (native-sensitive diff vs baseline commitb28a60adis empty). - Source commit:
d10f4bdd. App / runtime version:0.6.2(JS-only client). - OTA artifact: preview update group
3719d18f-2884-43ee-8238-dfaebe8a8142(Android019f2fcd-f381-7650-8faf-d15f36e9c0d7, iOS019f2fcd-f381-7bf1-800b-1be891e1c170), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(no native change). - Verification:
yarn typecheck+ fullyarn 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: revertd10f4bdd; the RPC + trigger changes are additive/CREATE OR REPLACE (restoring the priorassign_waitlist_positionbody + droppingstart_match/autolockreverts). - 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_directorcan'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 with42703(INSERT of a non-existentrsvps.created_at) → dropped the phantom column from both approve bridges; (2) a club could be permanently orphaned → BEFORE DELETE last-owner guard onclub_members; (3) a non-owner admin could disband/hijack a club via rawclubscolumn writes → reused the sharedprotect_columnsto guardarchived_at/created_by/type/invite_code/host_id; (4) owner-mint escalation →club_members_insertforbids insertingrole='owner'unless the caller is an owner; (5) invite-code join broken for ~98% of clubs (lowercase-hex vsupper()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
00311applied to the linked project viasupabase 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 prior00206/00304bodies 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(migration00310, supersedes00206) 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 protectedmatches.participant_idscache in sync — which is why it must be the SECDEF RPC, not a client.update()(the00067protect_matches_scoringtrigger reverts a plainauthenticatedwrite toparticipant_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 invalidatesmatchKeys.bySessionso the roster + draw reflect the substitute at once. Also hid the in-app 결제하기 (PortOne) rail for real users (mock adapter + dev-gateddev_mock_pay_sessionconfirm) — dev-only now; the manual 송금 완료 rail is the sole primary session-fee CTA. - Classification: migration (deployed) + OTA. Migration
00310applied to the linked project (llhvhhpbjnnrfwcczizc) viasupabase 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 commitb28a60adis empty). - Source commit:
658e2060. App / runtime version:0.6.2(JS-only client). - OTA artifact: preview update group
90326adb-e22b-41a5-9f54-829ed653f597(Android019f2c3a-302f-7cd8-8be8-d8d06e31ec4a, iOS019f2c3a-302f-7801-b554-5cf27604279a), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(no native change). - Verification:
yarn typecheck+ fullyarn 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_idssynced (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_idsdrops them, no error. - Rollback pointer: republish
0dbead44-.... Source: revert658e2060(client) + re-apply the00206forfeit_sessionbody to the linked DB (the00310CREATE OR REPLACE is additive; restoring00206reverts 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-existentsessions.cost_per_person→ dead; also wrote Korean intopayment_method, violating the 00285 vocab CHECK) → now usesparticipation_fee+ real vocab + the method/submitted_at split (00301); fixedguest_applicationsmissing a NOT NULLclub_id(00017) that failed silently every run; fixed DM unread always 0 (computed fromlast_read_at, 00289, and seeded threads were auto-read at creation) by rewindinglast_read_at. Added: a cancelled fee session driving the real refund trigger (00307refund_statepending/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-scenarioedge function shipped viasupabase functions deploy. - Source commit:
01498783. - Backend deploy:
npx supabase functions deploy seed-scenario --no-verify-jwtto the linked project (llhvhhpbjnnrfwcczizc). No migration (uses existing schemas/triggers). - Verification (THOROUGH, at scale):
eslintclean + deploy bundle valid + live runs.session_fee_manual(2 sessions) +club_growth(joinRequests=7) +home_pickupall seed clean. The fulldensechain proved it: dense_social now reportssessionPayments=904(was silently 0 — the dead generator) andreadState=258(was 0 — never seeded), with all 6 changes executing clean (no CHECK/NOT-NULL errors);dense_matchesseeded 1,472 matches,dense_computescored 1,000 withmatchErrors=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
matcheswere inserted one row per HTTP round-trip in nested loops (the team had batchedmatch_setsafter it "blew the wall-clock" but never applied it tomatches), anddev_reconcile_seeded_world(a full-DB rating replay) ran after EVERY chain step. Fixes: (1) batch the matches inserts —createSessions+seedLiveSessionaccumulate rows and do ONE.insert(rows).select('id'), mapping ids back to theirmatch_sets(mirrors the existing set-batching); single-matchhome_*sites stay single. (2)skipReconcileflag (mirrorsskipTruncate) — the dev-panel +run-scenario.mjspass 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_showcase25.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-scenarioedge function was deployed to the linked project (llhvhhpbjnnrfwcczizc); the only app-bundle change is the dev-panelskipReconcileclient (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(Android019f2abc-a1c1-7545-90fc-84c3ef29ce8b, iOS019f2abc-a1c1-7ca9-8ddb-d31692906bde), branchpreview. 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: revert3778e4ea(+ 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
timingslot (Pressable + Pencil,$primary, COMP-3 compliant), gated by the existingcanCorrect, callingopenCorrectiondirectly (1 tap) withstopPropagationso the whole-row → profiles-sheet path stays intact. Reused the previously-unusedmatchActions.correctScorekey (점수 수정 / 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(Android019f28ba-aa5b-7754-9d07-abef63f7865c, iOS019f28ba-aa5b-7d8d-b2a0-f1a63518f0b6), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+ fullyarn test(2,286) +lint:strict(0/0). - Redesign summary (phases 1–5, source commits): phase 1
d41e87cc(canonical labels/affordance), phase 297484a51+ migration00309(chat find-or-create), phase 3731b2edb(host lifecycle band CTA), phase 46fc7b3d6(flatten the 일정 관리 hub inline + kebab removal), phase 598d5376b(direct 점수 수정). - Rollback pointer: republish
3a2b9051-.... Source: revert98d5376b.
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 extractedSessionNavRow: 일정 수정 / 참가자 관리 / 경기 규칙 / 호스트 관리 / 출석 확인 / 경기 종료 (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 (SessionActionsMenudeleted): GPS 출석 체크 + 기권 render inline while in_progress + confirmed participant.edit-session-screenstripped to config-only (일정 수정) + a stale-deep-link fallback;NavRowlifted to sharedsession-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(Android019f28a3-168a-7b55-be09-b94e05518e7e, iOS019f28a3-168a-7521-b134-7c228818676c), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+ fullyarn 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: revert6fc7b3d6.
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). AddedsessionDetail.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(Android019f2883-b293-7f0a-bdbc-35146bd8b6eb, iOS019f2883-b293-7045-891e-0c9c148c6edf), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) + fullyarn test(2,286). Phases 4 (hub flatten) + 5 (score-row) pending. - Rollback pointer: republish
54de47f7-.... Source: revert731b2edb.
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_threadshad no session link). Now find-or-create. Migration00309_dm_thread_session_link.sql:dm_threads.session_idFK + partial unique index (dm_threads_session_group, ≤1 group thread per session);create_group_threadbecomes a true get-or-create (reuses the session's thread, folding in newly-confirmed members with no rate-limit/min-member gate, else inserts withsession_id; byte-identical when no session context). SECURITY DEFINER +search_path=''+ REVOKE/GRANT preserved. Port/adaptergetThreadBySession+useThreadBySessionhook +dmKeys.threadBySession+ create-group invalidation.startGroupChat→ existing threadroutes.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 --linkedapplied00309to the linked project (llhvhhpbjnnrfwcczizc) — "Finished supabase db push". Generated types regenerated + committed;check:supabase-typesOK. - OTA artifact: preview update group
54de47f7-3d53-44a2-a01d-f12706266893(Android019f286a-59fd-7a44-827f-058e4628b387, iOS019f286a-59fd-7592-880c-5939f203af8a), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - 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 revert97484a51. 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;
hostApprovedConfirmCta→hostApprovedNotice+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(Android019f2846-df2c-70f5-b352-7dc9a5aea2d4, iOS019f2846-df2c-7c3a-8f0b-4e44c5dbb4fd), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) + fullyarn 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: revertd41e87cc.
2026-07-03 Preview OTA 2eb45a5a — calendar dots cover past sessions too
- Change: the
DayCalendarPickersession 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: newrsvpport + adapterfindParticipatingSessionDates(distinctsessions.datevia the samesessions!inner(date)embed +.gte/.ltethe rangedfindPlayedSessionsuses), auseMyParticipatingSessionDateshook (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(Android019f2806-e688-7038-9e2b-a015802d8de7, iOS019f2806-e688-744f-ae3e-859e81679d06), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) + fullyarn test(2,286). - Rollback pointer: republish
a3a1c80a-206b-4875-8a9b-179edd1f9af8. Source: reverte974d057.
2026-07-03 Preview OTA a3a1c80a — flag audit close-out + calendar session dots + auto-collapse pickers
- Change: two commits. (1)
c31f27dbrefactor: the flag audit's last real item —record-detailDaySummaryCardstill hand-rolled the donut + legend that its siblingOutcomeSummaryCardalready delegates to the sharedRecapOutcomePanel; migrated it (donut + 3-line hero + full-width stat strip,SummaryChipgrid preserved) and removed the deadLegendItem+SegmentedDonutChartimport — the last duplicated donut block is gone. (2)eb450287home period-nav: a newpickAnchor(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; andDayCalendarPickernow 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_idisUUID NOT NULL(schema + ZodclubId: uuid()); pickup/번개 sessions still belong to a club, so there are noclub_id IS NULLrows 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(Android019f27d6-e6eb-7705-8b05-622703e9498b, iOS019f27d6-e6eb-7a60-951e-6b1a90b49473), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) + fullyarn test(2,285, incl. a new pickAnchor test). - Rollback pointer: republish
ded35b6a-3aee-4421-bbba-65063a6389c3. Source: reverteb450287+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, anduseTodayRecapSessionsaccept an optionaltargetDate;HomeTodayViewreads the store anchor (mirroring week/month) and renders aPeriodNavPanel+ a newDayCalendarPickerscrubber (‹ 년·월 › month stepper over a Sunday-first calendar grid; Korean weekend colors$errorSun /$calendarSatSat, muted spillover, selected-day fill, a reserved 오늘 marker line per cell — same anti-resize idiom as the month picker). The Live section is gated toisHomePeriodNow('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→ localaddDays), and makessessionKeys.activeAndRecentdated (per-day cache) with anactiveAndRecentAll(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(Android019f27af-befe-740d-8709-a65e0d7616e3, iOS019f27af-befe-73f7-8ccf-9e927a7eb910), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck(app/home/mobile/web) +yarn lint:strict(0/0) + fullyarn 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: revert01e0e7a1.
2026-07-03 Preview OTA ae89e06f — picker "now" markers + stable panel height + Phase 2c-A server-ranged match history
- Change: two commits. (1)
889fdf16home period-nav polish:PeriodNavPanelgets a fixed row height (TOUCH_TARGET.MIN) so the inline "on now" marker chip toggling no longer resizes it;WeekMonthPicker+MonthYearPickernow 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)58b71504Phase 2c-A:useMatchHistorygains optional{from, to}; the rsvp adapterfindPlayedSessionsembedssessions!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-sessionclub_id IS NULLRLS 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(Android019f278d-09fe-7148-97bd-84f4938616df, iOS019f278d-09fe-78ce-ab85-b3c0c5cee997), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) + fullyarn test(2,285). Phase 2c-B (day navigation) still pending. - Rollback pointer: republish
7b7ab6e6-a340-4760-9a44-ad9fc63b1053. Source: revert58b71504+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, matchingHeaderIconButton) so a near-edge tap steps the period instead of mis-firing the center toggle. (2) NewWeekMonthPicker(parallel toMonthYearPicker): 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(Android019f275b-3b03-7c4d-b98e-081ab84b8d93, iOS019f275b-3b03-7b3e-a944-9b9e157e0c65), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) +@twomore/apptests (141 suites / 2,283). - Rollback pointer: republish
6ff5231a-6e76-48e9-a4ab-a95c6b57a638. Source: revertfe1bcfa4.
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 canonicalDateSelection/deriveDateRange; selectorshomePeriodRange+isHomePeriodNow),PeriodNavPanel(one-row nav + inline anchored marker; center taps to open a scrubber only when provided — no chevron/reset), theaddMonthsutil, andhomePeriodLabel/homePeriodMarkerLabel. Wiring infeatures/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 aMonthYearPicker(year stepper + 12-month grid) to free-jump any month.HomeTodayViewuntouched — 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(Android019f272a-8636-7917-a6b9-1979d2c44539, iOS019f272a-8636-7c40-9e5b-2253174a646e), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) +@twomore/apptests (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: revert6281de71.
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, aDivider, 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) — realPeriodRecapdata that doesn't duplicate the donut count or win-rate hero.record-detailOutcomeSummaryCardadopts it while PRESERVING its richerSummaryChipgrid below (DaySummaryCardleft 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 strip9b4c950b+ workflow doc0ec8a422). App / runtime version:0.6.2(JS-only). - OTA artifact: preview update group
cb5c3cf0-430d-43d5-82e6-61e06d3702fc(Android019f26fa-a8d6-7c28-80b7-4eacb04dc039, iOS019f26fa-a8d6-72ec-9c50-9fdeec771d95), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) +@twomore/apptests (140 suites / 2,272). - Rollback pointer: republish
84653b70-29d5-46e2-b481-7225e970d5cf. Source: revert9b4c950b.
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-alignedcardMetalines → the canonical compactEmptyState(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 (displayMdnumber + label); the two hero numbers now align and the legend gets room.record-detail-screenshares 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(Android019f26c3-5298-7330-94d4-f79e3f08dfc6, iOS019f26c3-5298-734c-b1f4-33cb726fa286), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) +@twomore/apptests (140 suites / 2,272). - Rollback pointer: republish
4067f960-c09f-47ff-a4d1-e36b7ef401fa. Source: revertec839033.
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
VenueHeroWHERE bar now leading with an informativeCourtLinesTile(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+$primarytext →$text; WHERE bar$surfaceTertiary→$background+$primaryname →$text(court info stays$textSecondary; theCourtLinesTile/MapPinlead stays). TheVenueLinkpill 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(Android019f265a-37a2-7912-9d7d-e653cf38e8b0, iOS019f265a-37a2-754c-b40d-3e88a882aa65), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) +@twomore/apptests (140 suites / 2,272). - Rollback pointer: republish
bf4f1a6a-0bec-47fe-b9e0-80ee66f00227. Source: revert5d676649.
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 (
$surfaceTertiarywash +$primarytext) but theVenueHeroWHERE 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$textSecondaryhierarchy; theCourtLinesTilekeeps 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(Android019f2646-5d38-78e4-998a-ddbcf977b54c, iOS019f2646-5d38-76d9-b32c-3f6b64d98c3d), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) +@twomore/apptests (140 suites / 2,272). - Rollback pointer: republish
15ca8117-76a5-48e4-8320-fcfad85ae575. Source: revert35d4d88d.
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
$primaryClock icon from the session-header WHEN strip — the$surfaceTertiarywash +$primarydate/time text already carry the brand-green cue, so the icon was redundant. (2) Added the court-type primitive to theVenueHeroWHERE bar (the "venuerow"): a surface-tintedCourtLinesTilefor a known court surface, else the brandMapPin, 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(Android019f261b-cf52-7066-bd53-336a9a409220, iOS019f261b-cf52-7f55-b2a8-4b9f5aa704ae), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - Verification:
yarn typecheck+yarn lint:strict(0/0) +@twomore/apptests (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: revert26d94a1d.
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$primaryClock icon +$primarydate/time text — so the "when" strip mirrors the VenueLink "where" strip's green treatment. (2)VenueLinkpill tint alpha bumps ~14% → ~20% (PILL_BG_ALPHA24 → 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$primarySubtleveil. - 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(Android019f25e8-dcd2-7521-aa67-ac940565333d, iOS019f25e8-dcd2-76b0-affa-4a677b7db39c), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - 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: revert1a5b807d.
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-matchmatch.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) aboveMatchScoreboard, with status-aware scores (completed/live show real games + winner; scheduled show the ELO tug-of-war bar). Dropped the separate mutedProgressBar— a redundant second balance bar now thatMatchScoreboard'sVsDividerrenders 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(Android019f25a4-9e51-76ad-bddf-af6147c40476, iOS019f25a4-9e51-7e21-ba1b-936af0faa195), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - 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: revert1e0c5cca.
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"+ destructiveActionButton+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.
- settled → confirm → delete. Hidden on in_progress / completed (match records + ELO history). New i18n
- 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(Android019f2576-7055-7327-9268-0e693e9d8a7f, iOS019f2576-7055-7328-94e8-2206c5602b0a), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencepinned to build255bdfa0. - 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: revertb6d5b2d7.
2026-07-03 Preview OTA 0c2be8b0 — canonical config pickers + match dividers + configurable match rules
- Change: five session-management UX fixes. (1) New
Stepperprimitive (packages/ui) — the canonical ± number control (none existed). (2) Session-detail 경기 card gets the canonical inter-matchDivider inset="full"(between consecutive matches only), matching the 경기 page. (3) 일정 관리 config form: dropped the page-title subtext; swapped rawAdminFieldtext inputs for the create wizard's controls — native date/time pickers (DateTimePickerModal+PickerRow),StyledInput(venue/description), and the newStepper(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 nowSteppers, not fixed chips — domain schema widened (winBy1|2 literal → int 1-3;startingScore'0-0'|'1-1' enum → symmetric "N-N" regex string). The rule values are advisory/display only (server writesp_rulesverbatim; completion is by consensus), so no scoring-engine impact;round-rules-stripalready typedwinBy: 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-pickerwas already bundled for create-session).git diff b28a60ad..HEADover 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(Android019f2476-efb9-7347-bbd3-9572e42dfce3, iOS019f2476-efb9-7c65-80e0-804f14ba77c2), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0. - 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: revert9ae26bad.
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 tightPressable variant="icon"back arrow. NewHeaderIconButtonprimitive (packages/ui) gives every header icon one treatment — a tight 24px (xl) icon with a ~44pthitSloptouch 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.TriggerButton(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..HEADover nativeSensitivePaths empty). - Source commit:
91a5445b. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
0fdff3d9-5e36-4218-b61a-4965c093e7a7(Android019f2461-061c-7218-ac86-76e1f9436759, iOS019f2461-061c-7f38-a845-2080ed7d1ab5), branchpreview. Baseline advanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0. - 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: revert91a5445b.
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_correctionaccumulates participant consent and, on the last agreement, writes the corrected score (SECDEF bypasses theprotect_matches_scoringtrigger) + re-derives ALL ratings via the provenprivate.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_callcancels either type. Client:callTypeon 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 listconfirmed an only-00308 delta). No edge-function change (B2 reuses the existing 00240 score-call signal). Client ispackages/app+packages/features/sessions; no native-sensitive file changed since the 0.6.2 baseline (git diff b28a60ad..HEADover 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(Android019f20b3-8094-775b-b256-3e813aa51eaa, iOS019f20b3-8094-7d46-96c0-d00075cadbc0), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(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_ratingsis a GLOBAL rebuild (heavy but correct) — acceptable for a rare admin correction. - Rollback pointer: republish the prior compatible group
0611c21d-895a-4866-86ca-3203b2f3e003, oreas update:roll-back-to-embedded --branch preview. Source: revertb6047bd9. 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
paidrows 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; newhost_reliability_eventstable (durable accountability ding). Flow: a trigger onsession→cancelledopens a refund obligation on each paid row (excluding the host) + emits asession_refund_noticesignal; the unifiedcancel_session_as_hostRPC 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 persistenthost_refund_unsettledsignal; aBEFORE DELETEtrigger 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); thesend-pushedge function redeployed (2 new refund push titles). Client ispackages/app+packages/features/sessions+ i18n; no native-sensitive file changed since the 0.6.2 baseline (git diff b28a60ad..HEADover 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 --linkedapplied ONLY00307(remote was at 00306);npx supabase migration listdelta 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(Android019f208a-6c71-7a39-9dda-7c5788fbf5f2, iOS019f208a-6c71-75b0-8775-0c8d7f316eb4), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: revert6702f2eb. 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](ActionButtonsecondary+danger,flex1:1 — the canonical two-buttonBottomCtaBandpattern); 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 headerMessageCircleicon; 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 canonicalHostBadge(crown + brand-tintaccent; flips towarning"승인 N" when applications pend) replaces the loud$primaryhost strip on both the card (title slot) and detail (status-banner trailing slot — one strip, no second bar); removed the now-deadSessionTopBannerhosttone. - Known follow-up: the message icon opens the create-group flow (no persistent per-session chat room yet —
dm_threadshas nosession_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..HEADover 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(Android019f1eb7-eca4-7595-9e2b-e66c8272e05b, iOS019f1eb7-eca4-726b-b05a-6bc8cce73b7a), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: revert672db074.
2026-07-01 Preview OTA d83768f4 — edit → 일정 관리 hub + shrunk detail kebab
- Change:
EditSessionScreenis 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 forcanManageSessionat 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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
d83768f4-ddf7-4972-8a67-e5f5ae7c7bab(Android019f1db7-1d3c-70ee-9655-b7557460f90f, iOS019f1db7-1d3c-7914-a0e2-0cc3d7379a84), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: revert3c697da3.
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 —
SessionTopBannerhosttone (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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
80ffe522-33d3-4943-ad45-01f64f62eac3(Android019f1d93-a315-73b0-a485-b0828dcb804c, iOS019f1d93-a315-775c-81dc-dc2fb91c4d0b), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: revert23820d6f.
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_strikefor 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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
004516e0-779d-45c7-858d-70a2eb209cff(Android019f1d57-ae15-76d7-b087-78f4dfd5a09b, iOS019f1d57-ae15-70c5-845f-6256a4c9dc7e), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: revert5ea159c2.
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 —findOpenPickupsPagegained anexcludeCreatedByoption (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 — newsortSessionsHostFirst(date primary → host-first within day → time) applied touseMyParticipatingSessions+ 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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
c7eedd6b-5daa-4e46-9460-c5be4faf7bba(Android019f1d31-bae0-707a-b5a0-8a0e552b7823, iOS019f1d31-bae0-7d85-add1-38bc198726cf), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: revertc4d57010.
2026-07-01 Preview OTA b60a7171 — match-popup action suite (선수 교체 + 점수 입력)
- Change: the match popup (MatchProfilesSheet) now carries a permanent action suite.
MatchActionSuiterenders 선수 교체 (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/appi18n +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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
b60a7171-8477-496d-a2da-caa8548087e2(Android019f1b18-a0a3-77d9-9559-7db8ac48a61b, iOS019f1b18-a0a3-78d1-86ba-feebe3ac171c), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: reverte85bcf13.
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/appi18n +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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
1ecbd7ff-f4f1-4160-8730-b672b2daf1f1(Android019f1afb-c02b-764d-90af-af980cb4b79b, iOS019f1afb-c02b-7fc6-bc59-93655753a57d), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced;lastCompatibleSourceCommit+evidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: revertd3aa96a1.
2026-07-01 Preview OTA d6812462 — concurrent live sessions + defer text cards + match-swap impact editor
- Change: three owner items, two commits (
f079b2c8fixes,3a1ace93editor). (1) 오늘/이번 주/이번 달 only showed ONE live session —home-today-view+live-session-stackused.find(in_progress)→ switched to.filter()+ render all; week/month composed in-range live sessions from the cacheduseActiveAndRecentSessions(didn't mutate the sharedfindUpcoming, 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 pureanalyzeSubstitutiondomain fn (11 tests) reports the matches-played delta + same-round conflict + unconfirmed flag + repeat partner/opponent a swap would introduce; wired intoSwapSheetas an inline impact preview (advisory); swap gated toscheduledmatches 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) and3a1ace93a09a08f7e84ec03ed646500ace7bd231(match-swap editor). - Source sync: pushed to
origin/main; left/right count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
d6812462-e0fa-4378-848e-49af693561ec(Android019f19f8-fba2-7e51-a934-7391592d9d14, iOS019f19f8-fba2-7451-97d5-c5129f929429), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group;lastCompatibleSourceCommitevidencestay pinned to build255bdfa0(no native change).
- Verification:
yarn check(2,796 tests incl. 11 newanalyzeSubstitutiontests) +yarn lint:strict(0/0), all green. - Rollback pointer: republish the prior compatible group
364a3191-699b-4aac-9d67-1769a4778de8, oreas update:roll-back-to-embedded --branch preview. Source: revert3a1ace93thenf079b2c8.
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 →RoundSectionListfor structured sessions (sticky round header + grouped live-match bodies); ad-hoc stays standalone cards.LiveMatchRowgainedvariant='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;useViewerRoundContextnow exposescurrentRound. (4) 라운드 결과 tab →RoundSectionList(most-recent round open);CompletedRowgainedvariant='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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
364a3191-699b-4aac-9d67-1769a4778de8(Android019f19bf-f4af-7395-8a93-d8c9c48692d9, iOS019f19bf-f4af-7641-bcca-40cad0cd129c), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group;lastCompatibleSourceCommitevidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: reverteefeb1d1.
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
SectionListsection (sticky card-top header over a collapsible card-bottom body of embedded match rows). NewRoundSectionList(features) + a sanctionedStickySectionListprimitive in@twomore/ui(features can't import RNSectionListdirectly — COMP-5). Draws structured path converted; ad-hoc unchanged;RoundCarddeleted. (2) Match competitiveness — teams color-coded (team1 green$primary/ team2 amber$accentWarmleft-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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
6edd5d84-1a3b-4bb2-88de-7f932a1a5ce6(Android019f19a2-93bf-75bb-b386-6ccb3b243073, iOS019f19a2-93bf-70ee-ab88-767a2d44fd97), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group;lastCompatibleSourceCommitevidencestay pinned to build255bdfa0(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 AndroidSectionListstickiness (explicitstickySectionHeadersEnabled; verify it sticks, else add an animated fallback). - Rollback pointer: republish the prior compatible group
6abefd37-a574-4b34-94d3-ce7969edd482, oreas update:roll-back-to-embedded --branch preview. Source: revert6530bc1b.
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) NewRoundCard: 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-tabnow detects structured (matches span >1 round ORsession.rotationMinutesset) → RoundCards vs ad-hoc → standalone match cards.DrawsRowgained avariant='card'|'embedded'prop; round header content extracted toRoundHeaderContent(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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
6abefd37-a574-4b34-94d3-ce7969edd482(Android019f1975-056f-7006-a8a9-0e8d60d65b49, iOS019f1975-056f-7f07-95cb-d0c84c382ab1), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group;lastCompatibleSourceCommitevidencestay pinned to build255bdfa0(no native change).
- Verification:
yarn check(all workspaces green) +yarn lint:strict(0/0). The seeded live_showcase sessions setrotationMinutes, so they render as RoundCards on device. - Rollback pointer: republish the prior compatible group
b4acbbf6-1ccd-420d-9973-d25219e7bd16, oreas update:roll-back-to-embedded --branch preview. Source: revert1554f7b4.
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_showcaseseed 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_showcasechains), 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-jwtto projectllhvhhpbjnnrfwcczizc(bundle compiled clean). Ranlive_showcaseagainst preview: 30 users, 4 clubs, 16 live sessions, 206 matches in 28s. - Source commit:
1903a9c4e1e834c8f04851f4bafbbcc41c05e67b. - Source sync: pushed to
origin/main; left/right count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
b4acbbf6-1ccd-420d-9973-d25219e7bd16(Android019f1952-527a-79dc-b46e-21c45aa70b1f, iOS019f1952-527a-740b-86ec-c515c7ebc5f4), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group;lastCompatibleSourceCommitevidencestay pinned to build255bdfa0(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 inyarn typecheck(Deno) — it was typechecked by thesupabase functions deploybundle step. - Rollback pointer: republish the prior compatible group
db7b38f9-f58b-49e6-9b13-97f682e75dc2, oreas update:roll-back-to-embedded --branch preview. Source: revert1903a9c4. 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
paddingBottomhugs 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 (getExpectedScoreover effective team ratings), VS badge slides toward the underdog; neutral fallback for completed/unresolved (replaces the flat ProgressBar, removed from draws-row). (3) Popup: fullRoundRulesStrip(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 viauseFocusedEffect(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.tsxdeleted; 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..HEADovernativeSensitivePathsempty).
- Source commit:
81e0a079c37ffb483132009b10765dc1d8cd8d56. - Source sync: pushed to
origin/main; left/right count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
db7b38f9-f58b-49e6-9b13-97f682e75dc2(Android019f1924-7e44-73c3-b67d-2083af85d478, iOS019f1924-7e44-7551-8b72-7fb5edd37b54), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group;lastCompatibleSourceCommitevidencestay pinned to build255bdfa0(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, oreas update:roll-back-to-embedded --branch preview. Source: revert81e0a079.
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 →
Badgeneutral sm. Match cards gained an ELO-balanceProgressBar(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 newgetExpectedEloOutcomesdomain helper (hidden for completed/cancelled and when ratings can't resolve). - Classification: OTA-safe JS/TS —
packages/appdomain rule + i18n +packages/features/sessions/src/scorecard/*; no native-sensitive file changed since the 0.6.2 baseline (verifiedgit diff b28a60ad..HEADovernativeSensitivePathsis empty). - Source commit:
9af4af62b3a2d997f60857cb60f327a85d4b7cf0. - Source sync: pushed to
origin/main; left/right count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
2aa80eee-89fa-4908-8551-7855ece362e3(Android019f18da-2f76-7414-834c-be8572235553, iOS019f18da-2f76-784b-99b3-7ffa966e554d), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group;lastCompatibleSourceCommitevidencestay pinned to build255bdfa0(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 newgetExpectedEloOutcomescarries 3 unit tests (even ±16, favored, underdog). - Rollback pointer: republish the prior compatible group
7f6fa03d-6b61-4ebf-b98a-5806c418556d, oreas update:roll-back-to-embedded --branch preview. Source: revert9af4af62.
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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
7f6fa03d-6b61-4ebf-b98a-5806c418556d(Android019f1883-e9d5-7b25-a04e-715ebd45b8fe, iOS019f1883-e9d5-7ec1-a8a1-ac164e89802e), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check+yarn lint:stricton the 2 touched files green. - Rollback pointer: republish the prior compatible group
c1678d45-4aa4-49f3-8b72-448922f85705, oreas update:roll-back-to-embedded --branch preview. Source: revert942db9a9.
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/ensessions.ts; no native-sensitive file changed since the 0.6.2 baseline. - Source commit:
b8458d49fbc5d62562e29d1128fc4da6ed5dd0f4. - Source sync: pushed to
origin/main; left/right count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
c1678d45-4aa4-49f3-8b72-448922f85705(Android019f1846-19c6-7eea-9078-633412a3694f, iOS019f1846-19c6-7f62-b33d-63135d8cd5b9), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check+yarn lint:stricton 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, oreas update:roll-back-to-embedded --branch preview. Source: revertb8458d49.
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/ensessions.ts; no native-sensitive file changed since the 0.6.2 baseline. - Source commit:
a4cfd10738920ea9e186ef8f674300b0ae679fe2. - Source sync: pushed to
origin/main; left/right count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
bfcb4788-7f05-4e2d-aa4e-a8c875eaba33(Android019f182c-8bb6-7b75-93a5-e9f108698d0a, iOS019f182c-8bb6-7593-9653-04474fab425e), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check+yarn lint:stricton 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, oreas update:roll-back-to-embedded --branch preview. Source: reverta4cfd107.
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 —
$surfaceSecondarystrip, no border, 44px (TOUCH_TARGET.MIN), date left + time right, bothcardBody/600. One a11y node spoken naturally (오전/오후, no slashes/tilde) viaformatDate+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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
c56531e2-74a5-4d49-9262-ed64602e2e56(Android019f1801-2adc-7ba2-8a3b-bf49dc7bd6be, iOS019f1801-2adc-7f05-aefc-da348e7230e0), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check+yarn lint:stricton the 2 touched files green. - Rollback pointer: republish the prior compatible group
65ba4351-0f9d-43ba-8c2a-6962a9aed7d1, oreas update:roll-back-to-embedded --branch preview. Source: revert9431d746.
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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
65ba4351-0f9d-43ba-8c2a-6962a9aed7d1(Android019f1761-f855-7284-962a-38e9c814c64b, iOS019f1761-f855-7d61-9bb5-40df67e30c9e), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check+yarn lint:stricton the 3 touched files green. - Rollback pointer: republish the prior compatible group
1bb4d1a4-9d81-4cd5-844a-87f561d6913c, oreas update:roll-back-to-embedded --branch preview. Source: revertd49994dd.
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 belowcardTitle) 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.tsxonly; no native-sensitive file changed since the 0.6.2 baseline. - Source commit:
5e9f255bd8f5b20a79203a82016283650db69089. - Source sync: pushed to
origin/main; left/right count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
1bb4d1a4-9d81-4cd5-844a-87f561d6913c(Android019f1745-233c-7046-8ef5-2139afb87608, iOS019f1745-233c-7174-8488-d7cf6a91e8b6), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check+yarn lint:strictonvenue-hero.tsxgreen. - 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, oreas update:roll-back-to-embedded --branch preview. Source: revert5e9f255b.
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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
8feb7a9b-ef6d-4163-b77b-39bae790955c(Android019f1721-6a64-7df7-8c44-ba703cdb9af4, iOS019f1721-6a64-7ad1-bb35-5b144a732888), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check+yarn lint:stricton 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, oreas update:roll-back-to-embedded --branch preview. Source: revertc99b3c83.
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 fromsession.rotationMinutes· approval · payment →참가비right). Fee chip reads the canonicalsession.participationFee, not the legacycostTotalsplit. Scoped to the dev-gated location-first layout (newchipRowTwoTier); the production defaultchipRowis 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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
96ebedb7-e209-4045-98c5-f43616c4013e(Android019f16ce-dc48-73d3-9294-2c07a00b9ada, iOS019f16ce-dc48-7cfa-a32f-84727c14b111), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check(typecheck validated the new i18n paths + tests) +yarn lint:stricton the 2 touched files green; pre-push gate passed. - Smoke: install the 0.6.2 preview build (
255bdfa0), enablesessionSummaryPolishModeon 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, oreas update:roll-back-to-embedded --branch preview. Source: reverta76e9704.
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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
f7eadce5-c876-4393-ab8f-d689ddf99659(Android019f16b4-097c-7fce-b5f1-01ab34235e32, iOS019f16b4-097c-730d-81dc-3058e1d4a588), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check+yarn lint:stricton the 2 touched files green; pre-push gate passed; guard published. - Smoke: install the 0.6.2 preview build (
255bdfa0), enablesessionSummaryPolishMode; 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, oreas update:roll-back-to-embedded --branch preview. Source: revert5f53f242.
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 toMM/DD (요일)via a new year-lessformatMonthDaySlashWithWeekday(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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
77e93984-35e6-4eea-82d4-5e0404e6f0f5(Android019f1690-d797-7f02-83e8-73b2032af98b, iOS019f1690-d797-7c0e-93ac-f8c4e69f1d87), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check(typecheck + tests) +yarn lint:stricton the 3 touched files green; pre-push gate passed; guard published. - Smoke: install the 0.6.2 preview build (
255bdfa0), enablesessionSummaryPolishMode; 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, oreas update:roll-back-to-embedded --branch preview. Source: revertbb2c4dcf.
2026-06-30 Preview OTA d26fa9b8 — date/time inside the location-first map hero
- Change: on the dev-gated
sessionSummaryPolishModelocation-first SessionCard, the date/time moves off a separate pill row INTO theVenueHeromap 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 bumps132 → 156so the bars frame the centred pin rather than obscure it. - Classification: OTA-safe JS/TS client change — only
.tsxpresentation 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 count0 0. - App / runtime version:
0.6.2(unchanged — JS-only). - OTA artifact: preview update group
d26fa9b8-3853-4389-af9e-f5d2c1dbb53b(Android019f1664-47c4-7b8e-bac4-89c39dac557a, iOS019f1664-47c4-755b-9265-7e66644702e0), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Verification:
yarn check(typecheck + 43 tests) +yarn lint:stricton the 3 touched files green; pre-push gate passed; guard published (no native-sensitive change since baselineb28a60ad). - Smoke: install the 0.6.2 preview build (Android
255bdfa0), enablesessionSummaryPolishModein 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, oreas update:roll-back-to-embedded --branch preview. Source: revert5efd89c7.
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 (commitebee90d6): court info renders without a venue-name overlay, every recruitment state tints the participation ring, and the QA seed is ~5× faster (lookup-firstcreateUsers+ gateddeleteTestAuthUsers). - 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(Android019f125c-1a44-7e67-aa51-47b101268dd5, iOS019f125c-1a44-740c-a01e-6fc93a267096), branchpreview. BaselinelastCompatiblePreviewOtaGroupadvanced to this group. - Backend artifact:
seed-scenarioedge function redeployed (the seed perf change). - Verification:
yarn check+yarn lint:strictgreen;node scripts/guard-ota-release.mjs previewpassed (clean tree, synced, no native-sensitive change since baselineb28a60ad); 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, oreas update:roll-back-to-embedded --branch preview. Source: revertebee90d6/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.2and 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 commit111dbf77. - 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:
e9c30c57bumped app version0.6.1->0.6.2(RVO-4 native/runtime decision);b28a60ad7d3bc28d615fac5f0eec3a9a6e2ad2f2(fix(mobile): add @react-native-kakao/social) is the build source;fa66e90be3baa47780c548da74cba0ccb493ba49re-baselined + published the OTA. - Source sync: pushed to
origin/main;git rev-list --left-right --count origin/main...HEADreturned0 0at 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 old0.6.1installs. - 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, runtime0.6.2, commitb28a60ad. Install:https://expo.dev/artifacts/eas/MuJ2LP3_7rYZiJKKrkHOHPa52LBSVEo94j3-dK_dqhg.apk.
- Build failure root cause:
@react-native-kakao/share@2.4.5'sandroid/build.gradledoesapi getWorkspaceProject("social")- a hard native dependency on@react-native-kakao/socialthat the library omits from its npmpeerDependencies(declares onlycore). 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(commitb28a60ad);socialitself needs onlycore, so the native graph is complete. - OTA artifact: preview update group
704b7bff-729a-4966-9372-aaba021f37f2(Android update019f10ff-dca9-7b09-b2a9-db4e38258699, iOS update019f10ff-dca9-7d23-a048-ab14c20ff1fe), runtime0.6.2, branchpreview, published from commitfa66e90b. 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 previewpassed (clean tree, synced, no native-sensitive change since the re-baselined commitb28a60ad); 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
255bdfa0on 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.2preview runtime. - Rollback pointer: republish the prior compatible preview update group, or
eas update:roll-back-to-embedded --branch preview. Source rollback: revertb28a60ad(+ 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/socialpeer 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 andHH:MM~HH:MMon 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 --branchshowed## main...origin/main. - Source sync proof:
node scripts/guard-ota-release.mjs previewpassed the clean-tree/upstream-sync gate for commit1113d4b5, 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.2becauseruntimeVersion.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 --runInBandyarn workspace @twomore/app typecheckyarn lint:strictgit diff --checkyarn 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 build6aa365f6-7814-4250-a708-a27843a919dc; compatible iOS build not recorded. The changed native-sensitive files named by the guard wereapps/mobile/app.config.js,apps/mobile/app.json,apps/mobile/package.json,package.json,packages/app/package.json, andyarn.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
1113d4b5to 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 publishyarn 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/mainwith 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-readinessyarn docs:checkgit diff --check
- Current blocker posture:
yarn report:r1-release-readinessstill 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
145bea94fd31ac73b92493270f201f548dfaf446if 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 --branchshowed## main...origin/main. - Source sync proof:
node scripts/guard-ota-release.mjs previewpassed the clean-tree/upstream-sync gate for commite717c3c1, 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 --runInBandyarn workspace @twomore/app typecheckyarn 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-readinessnode 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 build6aa365f6-7814-4250-a708-a27843a919dc; compatible iOS build not recorded. The changed native-sensitive files named by the guard wereapps/mobile/app.config.js,apps/mobile/app.json,apps/mobile/package.json,package.json,packages/app/package.json, andyarn.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
e717c3c1to 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 publishyarn 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 --branchshowed## main...origin/main, andgit log --oneline @{u}..HEADwas empty. - Source sync proof:
node scripts/guard-ota-release.mjs previewpassed the clean-tree/upstream-sync gate for commit7b5f9ea1, 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 --checkyarn lint:strictyarn 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 build6aa365f6-7814-4250-a708-a27843a919dc; compatible iOS build not recorded. The changed native-sensitive files named by the guard wereapps/mobile/app.config.js,apps/mobile/app.json,apps/mobile/package.json,package.json,packages/app/package.json, andyarn.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
7b5f9ea1to 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 publishyarn ota:preview.
2026-06-28 - R1 Readiness Local Env Loader
- Change: make
scripts/check-r1-release-readiness.mjsload local env files (.env,apps/mobile/.env,apps/web/.env, andapps/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.mjsnode 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_KEYenv file value is available. R1 remains blocked on missing app-link proof env, missing six Kakao template IDs, live.well-knownredirect, 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 toapps/web/.env.example, document the Kakao template env setup in credential/build guides, and extendscripts/check-r1-release-readiness.mjswith 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/mainin the docs/release handoff ending at2fba00b2. - 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.mjsnode scripts/check-r1-release-readiness.mjs --reportyarn docs:check
- Report output:
R1 env example coveragepasses 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
81c9909e4fc3c78287c0ffdd0c5df7ae1fd14e8bto 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.mjsplusyarn check:r1-release-readiness,yarn check:r1-release-readiness:eas, andyarn 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/mainuntil 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.mjsnode 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, livehttps://twomore.app/.well-known/apple-app-site-associationredirecting towww, missing EAS preview Kakao env names, Android preview build6aa365f6-7814-4250-a708-a27843a919dcbeing 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-a27843a919dcand ranapksigner verify --print-certs. Its public signing certificate SHA-256 fingerprint isF4: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:easis the strict R1 acceptance gate;yarn report:r1-release-readinessis the handoff-safe status command. - Rollback pointer: revert source commit
64ea165c7651226f4f9a316b7de581d1cc353a92to 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.jsonand extendscripts/guard-ota-release.mjsso 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/mainbefore 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.mjsyarn docs:checkyarn workspace @twomore/mobile preflightyarn check- pre-push hook
- expected-failing
node scripts/guard-ota-release.mjs previewfrom clean, synced22f214ff
- Expected-failing guard output: source sync passes, then preview OTA is refused because native-sensitive files changed after baseline
111dbf77eb590b2ee6a49b0c70bffc3c2a5a342b/ compatible Android build6aa365f6-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, andyarn.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.jsonbefore future preview OTA. - Rollback pointer: revert source commit
22f214ff10d32bdc054ed74d125dc40103e5fca9to 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-knowndeployment 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: seeded01/02/08/09, seeded10, then generated R1 share deep links. - Change: add
scripts/check-r1-evidence-workflow.mjsandyarn check:r1-evidence-workflowso 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 incf0ed805. - 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.mjsyarn check:r1-evidence-workflowyarn docs:checkyarn 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
4aecec580788a13bed7eec198bec2d48a6591306to 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.
2026-06-28 - R1-3 App-Link Remote Android Parser Guard
- Change: repair
scripts/check-r1-app-links.mjsremote Android validation by adding the missing comma-list parser used forTWOMORE_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.mjsyarn check:r1-app-links- local HTTP fixture for
yarn check:r1-app-links:remoteusing 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-associationreturnsHTTP 307tohttps://www.twomore.app/.well-known/apple-app-site-association, and thewwwtarget currently redirects.well-knownrequests 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
ee49c9d75f59cc6033596d75c45f76b22b85d13fto return the remote guard to its prior behavior. No OTA/build rollback exists because no artifact was published. - Known gaps: correct live
twomore.appassociation 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_KEYas a backward-compatible fallback for the preferredTWOMORE_KAKAO_NATIVE_APP_KEYbuild-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.mjsyarn check:r1-kakao-share- expected-failing
env EXPO_PUBLIC_KAKAO_APP_KEY=placeholder yarn check:r1-kakao-share:credentials yarn docs:checkyarn check
- Static proof output: normal mode reports the six missing
TWOMORE_KAKAO_TEMPLATE_*IDs plusTWOMORE_KAKAO_NATIVE_APP_KEY or EXPO_PUBLIC_KAKAO_APP_KEYas the native app key gate. Credential mode with onlyEXPO_PUBLIC_KAKAO_APP_KEYexported 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
6e40fe7ae77606946741d73ba06a6c964d41ec0eto require onlyTWOMORE_KAKAO_NATIVE_APP_KEYagain. 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.appassociation 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, andexpo-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 preflightyarn workspace @twomore/mobile typecheckgit 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
daf2a01dto 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/coreand@react-native-kakao/share, configure the Expo Kakao core plugin behindTWOMORE_KAKAO_NATIVE_APP_KEY, expose configured template IDs through Expoextra, 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.mjsyarn check:r1-kakao-shareyarn workspace @twomore/app test src/adapters/share/__tests__/share.native.test.ts --runInBandyarn workspace @twomore/app typecheckgit diff --check
- Build-preflight follow-up:
- Closed by source commit
daf2a01d6244578a9d5af3eef4000bfd133556ac;yarn workspace @twomore/mobile preflightnow reportsDependencies are up to date.
- Closed by source commit
- Static proof output:
yarn check:r1-kakao-sharepasses and reports the six missingTWOMORE_KAKAO_TEMPLATE_*env vars plus a Kakao native app key as the remaining Kakao gates. The preferred R1 build-time env isTWOMORE_KAKAO_NATIVE_APP_KEY;EXPO_PUBLIC_KAKAO_APP_KEYremains 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
fb2fd1bdto 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
kakaoTemplatemetadata to session, club invite, match result, tier promotion, stat-card, and leaderboard share composers, extend share-content schema coverage, and addyarn 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.mjsyarn check:r1-kakao-share- expected-failing
yarn check:r1-kakao-share:credentials yarn workspace @twomore/app typecheckyarn workspace @twomore/app test src/presentation/utils/__tests__/share-cards.test.ts --runInBandgit diff --check
- Static proof output:
yarn check:r1-kakao-sharepasses 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:credentialsfails untilTWOMORE_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, andTWOMORE_KAKAO_NATIVE_APP_KEYare 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
422037f2if 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, setTWOMORE_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.
2026-06-28 - R1-3 Remote App-Link Proof Guard
Change: extend
scripts/check-r1-app-links.mjswith--remotemode and add root scriptyarn 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.mjsyarn check:r1-app-links- expected-failing
yarn check:r1-app-links:remote git diff --check
Static proof output:
yarn check:r1-app-linkspasses and reportsTWOMORE_APPLE_TEAM_IDandTWOMORE_ANDROID_SHA256_CERT_FINGERPRINTSas remaining external gates.Remote proof output:
yarn check:r1-app-links:remotefails with:texthttps://twomore.app/.well-known/apple-app-site-association must not redirect; got HTTP 307 -> https://www.twomore.app/.well-known/apple-app-site-associationInterpretation: 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-knownassociation JSON.Rollback pointer: revert source commit
e6f60c28if the remote guard creates operational noise. No OTA/build rollback exists because no app artifact was published.Known gaps: fix the live
twomore.appdeployment, set Apple Team ID and Android SHA-256 fingerprints in the web deploy environment, passyarn 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.
2026-06-28 - R1-3 Invite-Link Custom-Scheme Hosted Proof
- Change: extend
scripts/prepare-r1-share-deep-link-flow.mjsso the generated R1-3 Maestro flow also openstwomore://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/mainbefore 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, channelpreview, runtime0.6.1. - EAS artifact: hosted workflow
019f0c63-85bc-7f19-9d9b-d449ed548eb1. - Backend artifact: none; reuses the deployed
seed-scenarioclub_growthfixture and itsshareInviteCode = JOINR12summary field. - Hosted pass evidence: job
019f0c63-865b-797f-b6ad-5dcfaa4221e2finishedSUCCESS; JUnit artifact019f0c67-620e-7a41-a623-64c95676e217reportstests=1,failures=0,time=43.0; Maestro results artifact019f0c67-6014-71f5-bdd1-7e9c4f533429includes command traces for all generated share links. - Smoke evidence: command trace opened
twomore://join-club?code=JOINR12, asserted클럽 가입하기, assertedJOINR12, tappedclub.join-code.submit, and asserted중랑 초대코드 조인 테스트. - Verification:
node --check scripts/prepare-r1-share-deep-link-flow.mjsyarn 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
c9d81257if 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.
2026-06-28 - R1-3 HTTP App-Link Static Config
- Change: make
https://twomore.appthe canonical R1 share-link domain, add iOS Associated Domain config forapplinks:twomore.app, add AndroidautoVerifyintent filters for/s,/m,/p,/lb, and/join-club, serve credential-gated.well-known/apple-app-site-associationand.well-known/assetlinks.jsonhandlers from the web app, and addyarn check:r1-app-linksto 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-linksgit diff --checkyarn workspace @twomore/web typecheckyarn workspace @twomore/app test src/presentation/utils/__tests__/share-cards.test.ts --runInBandyarn workspace @twomore/mobile typecheckyarn workspace @twomore/web build
- Build evidence: the web build emitted dynamic routes for
/.well-known/apple-app-site-associationand/.well-known/assetlinks.json. It still printed the existing non-blockingexpo-locationexport warning. - Guard behavior:
yarn check:r1-app-linkspasses the static source/config contract and reports the remaining external gates:TWOMORE_APPLE_TEAM_IDandTWOMORE_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
a6675678if 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/lbhosted 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:previewguard passed with clean tree and111dbf77synced withorigin/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 update019f0c2b-2fae-7354-9ef0-fcd290f57b9a; iOS update019f0c2b-2fae-763f-8729-dfe8098aa2df. - Compatible preview build: Android build
6aa365f6-7814-4250-a708-a27843a919dc, channelpreview, runtime0.6.1, completed2026-06-27T20:37:57.922Z. - Backend artifact: none; reuses preview migration
00306_public_share_previews.sqland the deployedseed-scenarioclub_growthshare fixtures. - Pre-fix failure evidence: hosted workflow
019f0c17-0e66-7ad8-b0b6-47c2eb9747effailed on"성동구 정기전" is visible; failure screenshot showed이 일정을 찾을 수 없어요. Root cause was the mobile/sreceiver using privatesessions_selectmembership-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-a66dc39e99effailed on"소식" is visibleafter 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-185df88e8ad4passed generatedr1-share-deep-links.yamlagainst the reused Android preview build. JUnit artifact019f0c35-bbfc-7d0c-90f6-fa939bae691ereportstests=1,failures=0,time=38.0; Maestro results artifact019f0c35-ba30-7022-b1e3-eb49653ef882includes command traces fortwomore://p/...,twomore://s/...,twomore://lb/..., andtwomore://m/.... - Verification: focused repository tests for public share preview adapter;
yarn workspace @twomore/app typecheck;yarn workspace @twomore/mobile typecheck; fullyarn check; preview OTA publish;node --check scripts/prepare-r1-share-deep-link-flow.mjs;git diff --check; hosted workflow019f0c32-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.1was3ea7b802-e498-4197-a081-0284f3f49c12; revert app source commit111dbf77if the fallback behavior must roll back. Revert test-tooling commit55a90126if 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, rootprepare:r1-share-deep-link-flow/e2e:r1-share-deep-linksscripts, ignored Maestro generated-output path, and targeted EAS workflowmaestro-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.1unchanged. - Runtime version:
0.6.1unchanged. - 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.yamlwith current preview IDs for profiled38632ed-69d3-4557-bc0b-03b54a460cf5, session59ffa609-b522-4293-9dc7-4d0686887102, club5a762dcf-081a-48cf-8aba-822dd4d56dd4, and match67c7dd68-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.yamldid 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/lbweb receiver pages plus OG images onto that RPC, expose server-renderedtwomore://fallback links, extendclub_growthwith deterministic share fixture IDs, and addyarn 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.1unchanged. - Runtime version:
0.6.1unchanged. - EAS artifact: none; no mobile OTA/build run.
- Backend artifact:
yarn supabase db push --linkedapplied00306_public_share_previews.sqlto the linked preview project;yarn supabase functions deploy seed-scenarioredeployedseed-scenarioto projectllhvhhpbjnnrfwcczizc. - Artifact-order note: the preview DB push and edge-function deploy happened during implementation before source commit
ce6a2318existed. 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-blockingexpo-locationexport warning fromuse-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-scenariosource if fixture output must roll back, and apply a follow-up migration to revoke/dropget_public_share_previewif 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.
2026-06-28 - R1-3 Share-Link Contract And Invite Prefill
- 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:previewguard passed with clean tree andbf3c8746synced withorigin/main. - Target: preview OTA.
- App version:
0.6.1. - Runtime version:
0.6.1. - EAS artifact: update group
3ea7b802-e498-4197-a081-0284f3f49c12; Android update019f0bd5-d1c4-71eb-96b3-813f3e227785; iOS update019f0bd5-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-blockingexpo-locationexport warning fromuse-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:previewguard passed with clean tree anda5d2a913synced withorigin/main. - Target: preview OTA.
- App version:
0.6.1. - Runtime version:
0.6.1. - EAS artifact: update group
ff478b2e-4058-49ce-8d0d-333b66eabaa9; Android update019f0bb0-b9bc-7c87-8581-d6aa951247a3; iOS update019f0bb0-b9bc-755f-beee-8a96b9f4e137. - Backend artifact: none; reused deployed
r1_create_club_zeroseed scenario. - Verification:
yarn workspace @twomore/clubs typecheck;yarn lint:strict; YAML parse of10-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-3d8ab65bbdfereused Android preview build6aa365f6-7814-4250-a708-a27843a919dc, seededr1_create_club_zero, and passed10-club-create-first-session-cta.yamlin1m 11s. Artifacts: JUnit report019f0bb5-212d-7c29-ae01-2c7b796d8360; Maestro results019f0bb5-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 update019f0b2a-3bea-7206-975e-23e714b8898b; iOS update019f0b2a-3bea-7435-9c8a-652a36909005. - Hosted smoke: rerun
019f0b37-4a40-7eff-8727-d31d22a43a43passed enabled flows01-login,02-main-tabs-smoke,08-club-growth-media, and09-club-join-funnelsusing Android preview build6aa365f6-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.sqlapplied to preview;seed-scenarioedge function deployed. - EAS artifact: update group
1ac260bd-fe8e-49e3-924d-b7ce4619217a; Android update019f0b09-7081-7b87-8bfd-5bcfd00ce38a; iOS update019f0b09-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.yamlstill red after the first OTA; later closed by the approval join stabilization entry above. - Source detail: Rebuild Log.
Entry Template
### 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
- Classify the change with Release Versioning And OTA Workflow.
- Commit and push the source before a significant OTA/build artifact is claimed.
- Run the artifact command only from the committed source state.
- Add or update one evidence entry here after the artifact attempt.
- If the artifact affects a roadmap slice, also update the active source pack or implementation slice.
- 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.