Release Evidence Ledger
Contents
- Current Policy
- Latest Artifact Posture
- Owed Before Launch
- Evidence Entries
- What only a device can show
- Entry Template
- Update Procedure
Status: Active Last reviewed: 2026-11-13 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 |
|---|---|---|
| App version and runtime | apps/mobile/app.json (name Dabusi) is at version 0.12.0 with runtimeVersion.policy = appVersion, so a build from the current source has runtime 0.12.0; .release-please-manifest.json agrees (apps/mobile 0.12.0, root 0.3.0). The version moved from 0.11.0 on 2026-10-14 (540edb91) because native dependencies changed. Current Project State generates the live values. | The adopted preview baseline, apps/mobile/ota-compatibility-baseline.json, is still runtime 0.11.0 (build e4583905). Current source therefore differs from it on purpose until the post-flat-v2 native build (W5). |
| Preview OTA | Latest preview OTA recorded in the docs is update group 1719d507 on runtime 0.11.0 (2026-10-06, the Codex landing; device List 52 in Device checklists). Preview OTA groups after 2026-09-05 are stamped in the device lists and the decision log rather than entered below, and lastCompatiblePreviewOtaGroup in the baseline file still reads 6631a5d0-af14-42f2-8ae9-1a3f884f97dc (2026-09-24). Preview OTA has been paused since 2026-10-14 by owner decision: the next native build waits for the flat v2 migration, the baseline stays on the 0.11.0 build, and the fingerprint guard refuses yarn ota:preview (decision log, 2026-10-14, "the build waits for flat v2"). | No OTA is recorded after 1719d507, and the guard is expected to refuse publishing from the current source: the rename changed the native fingerprint (4ce47469; decision log, 2026-10-14). The pause ends with the W5 native build (migration plan, Build waves): build, install smoke, baseline update, then yarn ota:preview resumes. Do not work around the guard. |
| Preview build | Latest adopted preview build: Android e4583905-ddec-45bb-bcc4-08620f8fb47b, runtime 0.11.0, compiled from source commit 13a0fa5d and recorded as the baseline on 2026-09-30 (2e9659c8, native fingerprint 2f488e4d01fca1cdabab675b2f9739802e6824f4); the emulator boot smoke passed and the owner's install smoke was still open on 2026-10-06 (device List 47). For 0.12.0 (2026-10-14), EAS preview build 61e9fde1 failed at Android lintVitalRelease (fixed by a9dcab26), and the 0.12.0 preview build bae319b1 exists but is not adopted. The earlier builds a55c851f (0.9.0) and 2a544fb5 (0.10.0) are entered below. | e4583905 is the binary the baseline, and every preview OTA since 2026-09-30, targets. The next native build is W5 of the flat v2 plan and ships the new mark; it becomes the baseline only after an install smoke and a baseline update (compatibleAndroidBuildId, lastCompatibleSourceCommit, nativeFingerprints). |
| Backend (hosted QA bed) | The linked hosted project is the QA bed, not production. Per the 2026-10-18 handoff it is at migration 00698 (00698_release_event_rsvps_on_leave.sql), reported by the previous lead session on 2026-10-17 and not re-checked, with seed-scenario redeployed on 2026-10-17. The newest backend deploys entered in this ledger are the Codex landing 00678-00690 with five edge functions (2026-10-06) and, before it, 00585-00598 with four edge functions (2026-09-05). After that, per the handoff and the migration plan (Build waves): 00691 faces members-only (a0c6c18d, 2026-10-11), the flat v2 data waves 00692-00697 (2026-10-16 and 2026-10-17) and 00698 (d78482b6, 2026-10-17). The June R1 artifact, migration 00306_public_share_previews.sql plus seed-scenario from source commit ce6a23185a322c51c2d31f301b0a82fc96f82bba, stays in the 2026-06 entries. | The hosted schema is ahead of the newest published client: no OTA is recorded after 1719d507 (2026-10-06) and preview OTA is paused. The handoff records complete_session as checked for older clients and the other hosted changes as additive, and marks that partly unverified. |
| Hosted smoke | History of the earlier flows (recorded as of 2026-06-28, run on build 6aa365f6): the flows below were rewritten in batch 59 (flow 10 included) and the share-deep-link flow they name was a generated one; the rewritten flows have not run. 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 blocks OTA from dirty, no-upstream, unpushed-ahead, behind-upstream, and preview-native-incompatible source states. Since 2026-09-30 native compatibility is proven by the Android native fingerprint of apps/mobile (@expo/fingerprint, default options) compared with preview.nativeFingerprints.android in apps/mobile/ota-compatibility-baseline.json, the hash EAS recorded for the baseline build; the dependency-manifest check is the fallback (cc529921, 86c20202, 35d0bce0). yarn check:release-versioning checks the independent Release Please ownership of the root package and apps/mobile (7971eeab, 2026-10-06). 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. Since 2026-10-14 the current source is intentionally blocked from preview OTA (see the Preview OTA row); the way out is the W5 native build, an install smoke and a baseline update. R1 acceptance should use yarn check:r1-release-readiness:eas; handoff can use yarn report:r1-release-readiness. |
| Mobile preflight | On 2026-10-14 expo install --check blocked the 0.12.0 preview build on nine Expo packages one patch behind the SDK 57 table; 540edb91 aligned them with expo install --fix (expo 57.0.26 to 57.0.28, expo-router 57.0.24 to 57.0.26, expo-updates 57.0.24 to 57.0.26, and six more). apps/mobile/package.json now declares expo ~57.0.28 and expo-router ~57.0.26, and check:expo-sdk-table is part of yarn check:static. No later preflight run is recorded here. The earlier record (2026-06-28, source commit daf2a01d6244578a9d5af3eef4000bfd133556ac, Expo SDK 55 patch alignment) is superseded. | The patch alignment for SDK 57 is in source. The build attempt that followed (61e9fde1) failed at Android lint, not at preflight. A preview build enters the baseline only after an install smoke (see the Preview build row). |
| 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. |
The Hosted smoke, R1-2 status and R1-3 status rows are carried forward from 2026-06-29; the 2026-10-19 refresh did not re-verify them.
As of 2026-11-14 (the readiness pass)
The table above is history and is not edited. Where a row has changed, this is its present truth.
| Area | As of 2026-11-14 |
|---|---|
| App version and runtime | Source is 0.12.0; no build of it is adopted. The build the owner starts is the first of that runtime. |
| Preview build | Not yet made as an adopted build. The recorded baseline is still the 0.11.0 build e4583905 with fingerprint 2f488e4d. The Android native fingerprint of the current source is d51e2ac2 (computed 2026-11-15 with the build service's variables; iOS 63971091). The first W5 build 4a0f7492 failed (S51: the splash drawable). The second, 0c226ef8 (source a99d0142e), finished but fails the boot-smoke (S52: the React Compiler skipped a hook called through an unnamed expression). The third is made from the batch 67 commit (same fingerprint; it carries the one-tap developer sign-in, decision log S6) and is recorded here when it has passed the boot-smoke. The baseline is updated after a build that passes the boot-smoke and the install smoke by the steps in the post-build checklist, never before. |
| Preview OTA | Paused since 2026-10-14; the guard still refuses on the fingerprint. The next OTA is the first after the new build. |
| Backend (hosted QA bed) | At migration 00767 (pushed with batch 58 on 2026-11-13; 00768 and 00769 are applied to the local database only, S45; read it with npx supabase migration list; the 00698 figure above is a historical report). Fourteen functions were redeployed on 2026-11-13 and send-push again after batch 57. portone-webhook and verify-portone-payment are not deployed on hosted. |
| Hosted smoke | The Maestro flows were rewritten (fourteen: twelve run by default, 05 and 07 run by number with their own accounts; signed in by typing, no credential in a file); none has run against a build. The consolidated hosted packet workflow has still not produced a green run. |
| Mobile preflight | npx expo install --check exits 0 ("Dependencies are up to date"), re-run 2026-11-14 after batch 58 applied the eleven patch updates (S43 item 7, closed). |
| Release process | New gates: check:test-clock (tests that read the real clock), scripts/check-maestro-flows.mjs (in check:static), and a read-only yarn qa:health and scripts/qa/hosted-ready.mjs for the server and the migration. See qa-automation. |
| Test accounts | A tester types the e-mail and password; no build profile holds a credential (credentials). |
Owed Before Launch
Release-gate items that are measured or decided before launch, not build blockers. Each stays here until it is closed by a recorded result.
- S48 (batch 63) pre-launch items. (a)
venue_source_observationsis readable by every signed-in person; add an RPC that returns only the fields the venue page shows, then close the table to clients. (b) 71 otherP0002sites in 55 functions answer as HTTP 500 though the client reads the hint; monitoring will count refusals as server errors. (c) closed in this batch by 00774 (revoke_consentand a client's directuser_consentwithdrawal row both refuse a required purpose; account deletion still closes every consent); the legal check before launch should read it. (d)process_seed_achievementstakes no argument and awards to every club member; scope it by an argument (the scenario calls it only against the local stack). (e) Achievements are awarded on the player's device: an award a client can write is an award a client can forge; decide whether that is acceptable for badges shown to others. - S9: one rating replay timed on hosted-sized data. Measured locally 2026-11-11: 8.2 s for 1,786 completed matches, linear; 11.5 s locally for a full replay on the seeded world. A score correction now replays a window (migration 00744, F3c), not everything: only the matches from the corrected one onward. The first correction after 00744 is live pays one full replay (history older than 00744 has no before-state; the replay writes it), and the owner may run
SELECT private.recompute_all_ratings()once, deliberately, to spend that cost before launch. A correction that meets one of seven named conditions (seedocs/canon/data-and-hooks.md) also falls back to the full replay. The replay's start-state fix is migration 00743. The hosted timing of either path is not measured. As of 2026-11-13: unchanged and still open; the first correction on a device is List 53 F2. Superseded by the paragraph below (batch 60, after migration 00769): the reasontied_history_orderno longer exists, and "every correction replays everything" is no longer true. Measured 2026-11-14 (batch 59 integration), local, on the large seeded world (full_qa+qa_readiness: 3,195 matches, 1,615 completed; each timing is a call made as the developer account with the 8 s statement limit lifted, in a transaction that was rolled back, machine load 4 to 5): one score correction (resolve_score_review(..., 'change')) took 15.8 s (20.8 s once under heavier load; the seed lane saw 13 s). The full recomputeprivate.recompute_all_ratings()took 14.5 s. A correction made after that recompute still took 17.4 s and 18.1 s: it is NOT the one-time full replay that the first correction pays. The window still falls back to the full replay, now with the reasontied_history_order(before the recompute it washistory_chain_broken): the seeded matches share timestamps and their cosmetic ratings are not a chain. So on a world of this size and shape every correction replays everything and exceeds the 8 s limit of the API role (the seed catches that one failure on a large world). Whether hosted data falls back is not measured: the cause on the seed is ties and a broken chain, not size alone. Not changed in this batch (the rating code is untouched); if real data ties history order, a correction needs the replay to run outside the API role or in slices. Measured again 2026-11-14 (batch 60 integration), after migration 00769, local, on the large seeded world (readiness_full: 3,325 matches, 1,908 completed; loaded in about 140 s). Each timing isprivate.recompute_ratings_from(match), the call a score correction makes, as the table owner inside a transaction that was rolled back, once with the statement limit lifted and once with the API role's real 8 s limit. (1) Before the world is reconciled (a freshreadiness_full; the history has no before-state and no chain, and 0 tied groups): a correction of the oldest match falls back to the full replay withhistory_without_before_stateand took 11.0 s (11.7 and 11.9 s on an earlier load of the same world, 3,417 matches); a correction of the newest match falls back withhistory_chain_brokenand took 15.2 s (9.4 and 10.0 s on that earlier load). Both were cancelled at 8.0 s by the 8 s limit. So on a large seeded world a correction cannot complete under the API limit until a privileged role reconciles the world once. The tie reasontied_history_orderno longer exists, and no other reason fires behind a tie. (2) The seed chains: the smallreadiness_qareconciles its own world (dev_reconcile_seeded_worldfinishes inside the limit).readiness_fulldoes NOT: the chain reportsworldReconciled: no (statement timeout)and skips the score correction, becausedev_reconcile_seeded_worldtakes 28.0 s on this world with the limit lifted and is cancelled at 8 s; it cannot finish under its own limit. A large seeded world is reconciled by runningprivate.recompute_all_ratings()once as a privileged role (the owner of the table; 10.4 s here), ordev_reconcile_seeded_worldwith the limit lifted. (3) After that reconcile (committed, local test data): the history holds 44 tied (player, discipline, timestamp) groups (88 rows) and the tie forces nothing. A correction of a tied player's match is a window of 67 matches, 354 ms under the 8 s limit, modewindow. A correction of the newest match is a window of itself: 53 and 55 ms, and completes under 8 s. A correction of the oldest match is also the window path, but its window is every one of the 1,905 matches after it: 11.1 s and 15.3 s, cancelled at 8 s; that cost is the size of the window, which is the owed work in S45 (3) (replaying only the players a correction can reach). No full replay remained, so no further reason was recorded. (4) Real play writes a before-state for each history row and a consistent chain, so a correction in play is expected to take the window path, as in (3): expected, not measured (only seeded worlds were measured). (5) Hosted: timing is still not measured, and whether hosted data ever falls back is unknown. Nothing was changed for these measurements. - S10 (closed 2026-11-12, follow-up batch C): the insert policy on
inter_club_challengeswas a hole. Found 2026-11-11 (read from the live local policy): an owner or admin of a club could insert, through the data API, a challenge that was alreadyaccepted, with scores and a winner, against any club and in any person's name. Closed by migration 00755: the INSERT policy is dropped, INSERT, UPDATE and DELETE are not granted toauthenticated(the table has one policy left, the members' SELECT), and a challenge's status, scores and winner change through four functions only (propose_challenge,respond_to_challenge,cancel_challengeand the settle job'sprivate.settle_challenge), a list a database test pins by name. Evidence:supabase/tests/00755_challenge_lifecycle.test.sql(a direct insert fails for an owner, an admin, a member, an outsider and anon, with and without a forgedacceptedstatus, scores or winner;propose_challengetakes no status, score or creator) andrespond_to_challenge.test.sql(a direct UPDATE of status or score fails). Not yet live on hosted: it is pushed withdb push; until then the hole stays open there (the hosted project is the QA bed). - S11: nothing built since the OTA pause has run on a device, in a phone browser, in KakaoTalk's in-app browser, or as the web app offline. Device QA is part of W5. As of 2026-11-13: still open, and now scripted: List 53 is the hand-test script for the 0.12.0 build, post-build checklist the order of the first day, and the flows a device alone can show are gathered in "What only a device can show" below. Fourteen phone flows were rewritten against the rebuilt pages and linted; none has run (the new build does not exist yet).
- S12 (closed 2026-11-12, batch 52): the board lint is in the gate with zero problems. It reported 13 recorded problems; twelve were defects fixed at the source (gap and height literals to the kit's spacing values, one retired word, the
DtlDmwrappers replaced by gaps, and the helper line the operator band drew in the no-draw state, which the app showed too: removed in both, the page's empty state says why). One takes the lint's ownkit-only: allowcomment with a reason a reviewer can check (thedata-insertwrapper inHubClubPaneHome, which the verifier reads). The word is 최고의 파트너 in the app and the boards.yarn docs:lint-boardsis the last step ofcheck:static; its baseline only shrank (397 to 363). Evidence:node scripts/lint-flat-v2-boards.mjsexits 0,yarn verify:flat-v2-boardsandyarn measure:board-heights --check(EXACT),scripts/__tests__/flat-v2-board-lint.test.mjs. - S13: the web shell draws nothing on the server (every web route waits for the browser's first measure), so a share page's content is not in the server's HTML; only its metadata and link card are. Kept, not launch-blocking (decided 2026-11-12): share pages are
noindexand their link card is already server-rendered, so it is a later improvement. - S14 (closed 2026-11-12, batch 52, migration 00759): joining a club from a link while signed out on the web.
get_club_invite_preview(p_code)(anon) returns six allow-listed facts of the club (never an id, logo, description, member, code or dues) behind a global cap of 2,000 calls per Korea day that fails closed; the join page shows the club above the sign-in band, and with no preview (unknown code, the cap, offline, an error) it is the page as before. Found on the way and closed with it: the signed-in lookup and join by code had no limit and codes are 32 bits, so they are now 15 lookups and 5 joins per person per hour, and a join succeeds only for a club the person found through the lookup within 30 days (the same refusal as a wrong code otherwise). Evidence:supabase/tests/club_invite_preview_and_throttle.test.sql(65 assertions: the key set, null cases, the cap, the throttle, the window, receipt indistinguishability),00026_secdef_acl_hygiene(the named anon grant),use-club-invite-preview.test.tsxandjoin-club-screen.test.tsx. - S15 (closed 2026-11-12, batch 52, migration 00758): the share preview no longer returns the stored title, the typed place or coordinates. A session or match preview carries the derived title only and the linked venue's name; a canary session titled
SECRET-TITLEatSECRET-PLACEyields neither string for the session or its match, as anon and signed in (supabase/tests/share_preview_derived_title.test.sql,public_share_preview_web_fields.test.sql); the app's parser reads old- and new-shaped payloads. Visible effect, accepted: a session with a typed place and no linked venue shows its derived title. - S16 (closed 2026-11-12, batch 52): 대진 짜기 holds a group stage the server would refuse.
groupStageFits(two teams at any group size; 7, 13, 14 or 15 teams at a group size of 3) draws the shape Notice, softens the band and makes nothing on the press; the server'stournament_config_invalidkeeps its own line. Evidence:draw-tournament.rules.test.ts(2 to 32 teams x group sizes 3 to 8 against the server's rule, and the shared case table),check:group-stage-shape-paritywithsupabase/tests/group_stage_shape_parity.test.sql(the SQL held to the same table), the draw screen tests. - S17 (closed 2026-11-12, batch 52): the kit's offline hold at its two joints is tested as pure compositions (the repo cannot render real Tamagui components in jest): the option gate chain of a
ValueSheetoption (value-sheet-visual.test.ts) and the people picker footer'sfooterGate(people-picker-sheet-visual.test.ts, used by the component). The React binding itself stays on the device list (S11). - S18: the 장부 내려받기 on a phone has never run. F2 cut 1 added
expo-sharingandexpo-file-system(packages/app,apps/mobile), so the native fingerprint changes and the path needs a build that holds them (W5). The native adapter writes the CSV to the cache directory and opens the share sheet; check on an iPhone and an Android phone that the sheet opens, the file keeps its name, and a spreadsheet app reads the Korean text (the file starts with a byte-order mark). The web download (a Blob link) is covered by tests only, not by a real browser. As of 2026-11-13:expo-file-systemis now declared inapps/mobile(it was only inpackages/app) andexpo-sharingwas already in; the build that holds both is the one the owner starts. Still never run on a phone: List 53 E5. - S19: a receipt from the camera is not built. No code opens the camera; the 장부 opens the album directly. A build-readiness audit (2026-11-13) found that the Android merged manifest already declares
android.permission.CAMERA(through the image picker's default), so the permission is present although nothing uses it. The native config fix (declare the camera permission on purpose, or remove it) is scheduled before the build, in the readiness pass. As of 2026-11-13: the config fix is made (readiness lane NATIVE): the image-picker plugin hascameraPermissionandmicrophonePermissionoff and a Korean photo-library sentence, so the merged Android manifest should carry noCAMERA. Introspected from the config only; the merged manifest of a real build has not been read. Settled whenaapt dump permissionson the built APK, or the phone's app-permissions page, shows no Camera (List 53 C7). - S20: a photo uploaded from the web app is re-encoded through a canvas, and no real browser has run it. The tests prove the steps over a stubbed canvas; jsdom can neither decode nor encode an image. Check in a real browser (Chrome, Safari, KakaoTalk's in-app browser): upload a geotagged photo as a receipt, a club photo, a post image and an avatar, then read the stored object's metadata (no GPS tag may remain), its orientation, its size against the preset, and a HEIC pick on Safari.
- S21: a refund opened by a cancel is not settled through PortOne. F2 cut 2 (00752) opens the same obligation on a PortOne row as on a manual one, but the money moves only on the manual rail (the host sends it, the payer's 환불받았어요 closes it). Settling a PortOne refund waits for the payment rail (Phase 8, where no real PortOne money exists yet). As of 2026-11-13: unchanged; the manual rail is List 53 E2 to E4.
- S22: the refund and the 매치's account never ran on a device, a phone browser or hosted. Not run: the paid player's cancel Dialog on both sides of the deadline, the host's 환불 대기로 바꾸기, the wizard's and 일정 수정's account group, the 입금 안내 Sheet for a 매치 and a waitlisted viewer's page. The Dialog's sentence comes from the device clock and zone while the server decides with its own clock at the cancel, so a cancel in the last seconds can be refunded or kept against what the Dialog said. The cron
purge-session-deposit-accountshas not run on hosted (nothing exists there to purge on its first run); its health is the cron watchdog's. The three edge functions (send-push,export-user-data,seed-scenario) are redeployed after the push, and 00752 and 00753 are pushed withdb push. As of 2026-11-13: now seeded (readiness_qa: a deposit account on a fee'd 매치, a seat holder not yet paid, a refund waiting, a refund received) and scripted on two phones: List 53 E1 to E4. Still not run. The hosted functionssend-push,export-user-dataandseed-scenariowere redeployed on 2026-11-13. - S23: a 매치's account cannot be removed on its own. The bank Cell has no empty choice, so the account clears only when the fee goes free or all three answers are emptied through a path the page does not offer. The owner decides whether 일정 수정 gets a 계좌 지우기 control.
- S24: a legal check on the 사유 인정 요청's category and note is the owner's to arrange before launch. The category 아픔, 부상 and the free-text note (at most 100 characters) can be health information (PIPA sensitive data). 00754 marks the
attendance_excusepurpose sensitive with consent as its basis, shows who can read it and when it is deleted on the Sheet, and keeps both fields out of every signal, push, log and persisted cache, but whether the consent wording and the 12-month retention are enough is a legal question this batch could not settle. - S25: the excusal request never ran on a device, a phone browser or hosted. Not run: the ask after a late cancel, the record's Sheet and form, the officials' pair, Dialogs and 되돌리기 Toast, the 홈 card, the 알림 rows, and the three cron jobs (
signals-excuse-reminders,signals-excuse-answers,purge-aged-excuse-requests; the table is empty on its first hosted run, so each does nothing then; their health is the cron watchdog's).send-push,export-user-dataandseed-scenarioare redeployed after the push, and 00754 is pushed withdb push. A 매치's creator who is neither owner nor admin of its club gets the notice and the 알림 row but cannot open the officials' record (recorded gap). As of 2026-11-13: seeded (four people in four states) and scripted: List 53 F4. Still not run. - S26: the 클럽 대항전 loop never ran on a device, a phone browser or hosted. Not run: proposing (the form, the opponent picker, the plan Sheet), 철회 and 취소, the three settle states of a row, the tag in 경기 유형 and in 일정 수정, the Feed item's "vs" title, and above all a member of the other club on an installed build (an old build may not draw the apply button; the build after this one is the first with it): the notice, 매치 미리보기, the application, the confirmed seat, the host club's account, the match played and the settled notice. The 90-day reach is a line under the period, not under the Calendar.
send-pushis redeployed after the push, and 00755 to 00757 are pushed withdb push. As of 2026-11-13: a proposal waiting for the developer account (an admin of the other club) and a settled challenge are seeded: List 53 C9. The case of a member of the other club on an installed build is not scripted: the seed source names no member of the other club besides its owner (the states each account holds are in List 53, "Accounts"; the local run passed 22 of 22 on 2026-11-14). - S27: the settle job's first hosted run will rewrite old challenges by its rule. The hourly job
settle-due-challenges(minute 25;private.settle_due_challenges) freezes everyaccepted(or legacyin_progress) challenge whose end date is more than 7 days past. No migration statement rewrites a row; the job does. The QA world's seeded challenge (storedin_progressat a hardcoded 1-1, with no tagged session) becomescompleted, 0-0, no winner,counted_matches0, and the members of both clubs get one settled notice (a number of pushes to expect on the first hour). Any seeded challenge past that window does the same. The standings and the Feed read it from then on. Aproposedchallenge past its end date still holds its pair until it is withdrawn or declined (a recorded gap: no expiry is built). Live score calls (match_score_calls) stay members-only, so a player of the other club does not see the live strip.
S28: the signed-out invite page and the anonymous cap have not run in a real browser or on hosted. Batch 52 (00758 to 00760) is proven by database and jest tests only. Not run:
/join-club?code=...signed out in a phone browser and in KakaoTalk's in-app browser (the club above the sign-in band, then the page unchanged when the preview is absent), and the cap on hosted (2,000 calls per Korea day, all visitors together: one abuser can spend it and turn the preview off for the day, and the page then looks as before; per-IP fairness is a later step). 00758 to 00760 are pushed withdb push;seed-scenariois redeployed after the push (its wipe list gainedprivate.invite_lookup_receipts). As of 2026-11-13: scripted: List 53 K1 and K2. Still not run in a real browser.S29: the developer account and the developer functions' switch go off at launch (owner, 2026-11-12: recommended practice). One password on the hosted project reads all telemetry and moderation reports if the developer account exists. The engineering pass (wave 1) builds one helper that requires both the account and the switch; the QA bed needs the developer panel until launch, so the hosted change is owed then: the switch off and the account disabled on the production project. The same decision removes the preview build's auto-filled test password. Built (wave 1, migration 00761):
private.is_dev_caller()is the account ANDapp_config.allow_dev_rpcs; all nine developer functions refuse with the switch off, proven bysupabase/tests/00761_developer_gate.test.sql. The preview profile no longer carries the password: the developer sign-in's password field is empty and the owner types it on the new build. Owed at launch, on the production project:allow_dev_rpcsset tofalse(or the row removed) and the developer account disabled. As of 2026-11-13: the build-side half is done. The preview and development profiles no longer carry the developer e-mail or password, andEXPO_PUBLIC_SHOW_DEV_TOOLSwas removed (read by nothing); everyenvobject ineas.jsonis empty. The developer account's password on the hosted project was rotated on 2026-11-13 and lives only in two untracked env files; the value in the git history is dead. Seeded accounts' password moved out of source to the function secretSEED_ACCOUNT_PASSWORD. A tester signs in by typing (credentials). Still owed at launch:allow_dev_rpcsoff and the developer account disabled on the production project.S30: a cold start with no network shows no cached data. The disk cache is written during a session and deleted at every start (
beginSessionBootstrap→clearQueryCacheMmkvStrict, since 2026-09-07, so one account's cache can never show to another); offline display works only while the app stays in memory. The wave-1 allow-list (PERSIST_TABLE) limits what is written to disk but does not change this. An account-stamped, encrypted cache that restores for the same account needsexpo-crypto(a native module) in the build and is built after it, as an update once the build has passed device QA. As of 2026-11-13: unchanged by design; List 53 A7 states the expected page (the offline page, not cached lists; the old List 47 row 3 no longer holds).S31 (applied on hosted 2026-11-12 at 16:40): migration 00761 (wave 1). It was pushed with
db pushafter its commit and the post-push database suite; read-only schema probes confirmed the new objects. It revokes EXECUTE on 27 trigger functions and five helpers, bounds anonymous inserts intoclient_errorsandclub_growth_events(an old installed build writes within the bounds: its largest row is a 4,000-character message, an 8,000-character stack, an unbounded route and context, so a route over 500 characters or a context over 32 KiB from an installed build is refused until it updates; the new build caps both), and replaces nine developer functions. No edge function changed.S32 (applied on hosted: 00762 on 2026-11-12 at 19:36, 00763 at 23:16; 00764 and 00765 not yet). 00762 (wave 2) adds the sink for swallowed server errors, narrows or records the
WHEN OTHERSblocks, replaces the cron watchdog (counts failed runs, notices a job that has not run, reports a job without an expected interval) and extends the 30-day anonymization. Each was applied after its push and the post-push database suite; read-only schema probes confirmed the new objects. Still owed, and not readable without a hosted data read (which needs the owner's word): (1) the watchdog's first verdict: runSELECT private.check_cron_health()once and readstale_detectedand the alerts it wrote; a hosted job the local list lacks is registered from its schedule text (a fixed-time list reads as daily, a schedule it cannot parse as 31 days), so check each derived row inprivate.cron_expected_intervalsagainst the job's real cadence; (2) the withdrawal scrub's first run: the job selects every profile withdrawn more than 30 days ago that is not anonymized, or was anonymized earlier but whose sign-in record is not yet scrubbed (at most 100 a run), and scrubs theauth.usersrow, deletes everyauthrow keyed to the person and the audit-log entries naming them, deletes their private match notes and invite lookup receipts, and nulls feedback notes, invited phones and the club nickname (readSELECT count(*) FROM public.profiles WHERE deleted_at < now() - interval '30 days'first); (3) what the swallowed-error sink holds (SELECT source, count(*) FROM private.swallowed_errors GROUP BY 1). The legal check (S24) is still open on what stays: posts, comments, messages and reviews remain readable under the anonymized id. Not exercised: the privilege path of a hostedauthtable the owner cannot delete from (recorded to the sink and skipped); only the local database ran the walk. 00764 and 00765 are the only migrations not yet on hosted; the lead applies both after the wave's push. 00764 (indexes, grants, money): the five money checks are addedNOT VALIDand then validated byprivate.validate_constraint_or_record. If an old row offends, the helper DROPS that constraint (aNOT VALIDcheck would refuse every later update of the row, for example the status job over a session with a negativecost_total) and records the table, the constraint and the count of rows inprivate.swallowed_errors; the migration never leaves a row that cannot be updated. After the push read which of the five exist and are validated:select conname, convalidated from pg_constraint where conname in ('sessions_cost_total_nonneg', 'session_templates_cost_total_nonneg', 'clubs_dues_amount_nonneg', 'clubs_default_guest_fee_nonneg', 'club_events_cost_per_person_nonneg')should return five rows, all true. A missing row means an old negative row exists: readselect detail from private.swallowed_errors where source = 'validate_constraint_or_record'(the table, the constraint and the count, never a value), repair the rows, and add the constraint in a later migration. The migration's locks (the constraint's ACCESS EXCLUSIVE and the index builds) are held to commit; the largest local table holds 2,688 rows, hosted sizes are unknown. The optional read-only pre-check of the five columns for a negative value needs the owner's word. 00765 (security closure): after the push run the database suite and the smoke in S33. Edge functions to redeploy and smoke after the push: see S33. Answered (hosted, 2026-11-13): all five money checks exist and are validated; none was dropped, so no old negative row exists and no later migration is owed for them. As of 2026-11-13: hosted is at migration 00767 (pushed with batch 58 on 2026-11-13; read it withnpx supabase migration list), so 00764 and 00765 are applied, the five money checks are validated (answered in the paragraph above), 00766 and 00767 are applied. Still owed and needing a hosted read: the watchdog's first verdict and the sink's content, which the developer-gatedyarn qa:health --hosted --runnow reads in one call (needs the owner's word andallow_dev_rpcson). The withdrawal itself is List 53 H6; the day-30 scrub is a server job a hand cannot see.S11 (2026-11-12, engineering pass wave 3): enforce the four bundle schemas after a release has run clean. The club detail, home snapshot, session card and session detail bundle results are checked against zod schemas (
adapters/supabase/bundle-schemas.ts,parseBundleinbundle-wire.ts); today a result that does not match is reported once per bundle per session (issue paths only) and still returned as it was, so no screen changes. The schemas hold on 125 real local payloads. Owed: read the report counts from one released build over a week; when none arrives,parseBundlethrows a typedInvalidResponseErrorinstead of returning the cast value. Until that read is recorded here, nothing is enforced.S33 (2026-11-13, security closure): deploy, configuration and smoke after the push. The edge functions changed (constant-time secret comparison through
_shared/secret-compare.ts, stable error codes, the webhook hears a cancellation,seed-scenariorequires an allow-list,ingest-venuesanswers no browser): redeploycheck-push-receipts,send-push,fetch-weather,fetch-living-weather,fetch-medium-forecast,fetch-weather-warnings,enrich-venues,ingest-venues,venue-maintenance,corroborate-venues,simulate-activity,portone-webhook,verify-portone-payment,seed-scenario,search-venuesandstatic-map(never with--no-verify-jwt). Nothing could run the Deno entrypoints here (nodenobinary): a bad import path would fail only at deploy, so smoke each once after the deploy: a cron-secret call answers 200 and a wrong secret 401.seed-scenario: the allow-list gates people, the service key is a credential by itself. A caller with a person's token is refused 403allowlist_not_configuredwhenSEED_ALLOWED_UIDSis unset (before Auth is asked) and 401user_not_in_allowlistwhen the person is not on it, in every environment. A caller with the server's service key is answered exactly as before: every environment gate stays (a hosted deployment needs the owner unlock, which itself requires a non-empty allow-list), and the allow-list is not asked, soyarn qa:seed --localandrun-scenario --localwork without editing an env file. Set the hostedSEED_ALLOWED_UIDSbefore the owner seeds with a person's token. Done for 14 functions (hosted, 2026-11-13): each boots and refuses a wrong or missing secret.portone-webhookandverify-portone-paymentare not deployed on hosted at all and go out with the card rail. The right-secret cron path is unproven until the server-health read of the readiness pass. As of 2026-11-13 (after batch 57):send-pushwas redeployed again.portone-webhookandverify-portone-paymentstay undeployed on hosted until the card rail has its console secrets; the owner's hostedSEED_ALLOWED_UIDSandSEED_ACCOUNT_PASSWORDare set before any hosted seed.S34 (2026-11-13): sign-out clears the push token on the server only while online. Offline, the app signs out on the device and makes no server call, so the previous account's token stays on the server until the next account registers the same token (one profile per token: the new registration clears it from the old profile, proved by
security_account_and_payment_closures). Remaining gap, a device-check item for the build: the previous account's notices can reach the phone between an offline sign-out and the next registration on that phone. Needs the W5 build and a phone to see. As of 2026-11-13: scripted: List 53 I4. The network lane also rewrote the push-token lifecycle (a permanent failure is reported once and never retried; a network failure retries up to three times; a rotated token re-registers). Not run on a device.S35 (2026-11-13): the payment webhook hears a cancellation and cannot act on it. No database function releases a hold on a provider's cancellation, and the money rule is the owner's. Before the card rail goes live: a privileged
apply_portone_cancellation(looks up the hold by the provider's payment id, reverses it idempotently under the refund rules ofdocs/canon/payments.md, notifies the host) and a durable record for a cancellation that matches no hold. Until then a cancelled card payment leaves its hold paid and is only logged. The card rail has no client yet, so nothing reaches this today. A provider 404 for the id of a cancellation or failure event is acknowledged (200, logged asunknown_payment): an id PortOne does not know cannot become known by retrying; any other provider error still asks for a retry, and the paid path still retries a 404.S36 (2026-11-13, closed by batch 57): a person becomes a member only by their own act (N13), and the person's side is built. Migration 00765: a nomination or a guest conversion creates a pending offer (
club_join_requestwith sourceadmin_nominated);decide_join_requestrefuses to accept it (hintdecide_nominee_must_accept);answer_club_nomination(request, accept)is the person's answer. Migration 00766 and the pages close the loop: the person is told (club_membership_offered, a request notice), sees the offer on the club's row in the 클럽 tab, and answers on the club's page (수락, or a quiet 거절 that asks once); the officials see the offer as waiting, can cancel it (제안 취소), are told of a decline (club_membership_offer_answered, on a decline only), and an accept reaches them as the member-joined notice. An official can offer the same person at most three times a week per club (hintmembership_offer_rate_limited). One rule ends an offer without a yes (the guest returns to active; removing a guest cancels the offer). Not verified on a device or on hosted. As of 2026-11-13: the pages are on two-phone scripts, List 53 C1 to C3. The guest shortlist word is 가입 후보 and the offer is 가입 제안 (terminology guide). Not run on a device.S37 (2026-11-13): security findings not closed in the closure, each with its reason. F8 profile-field privacy (what other signed-in people read of a profile): the readiness pass. F12 the developer password in the development build profile: the owner rotates it on hosted (S29). F14 code-signed updates: the W5 build. F15 enforcing the content security policy: after a reporting window (the web sends it report-only with a report route; collect reports, then nonce-based scripts and the confirmed Naver Maps host list; the host list is from documentation, not from a browser). The
deleted_at IS NULLtest in the core write policies (a withdrawn profile keeps write rights until the day-30 scrub; size M, many policies). A global per-minute cap on rating replays: measured locally 8.2 s for a 1,786-match replay and a limit of 10 corrections an hour per account, so about 45 colluding accounts could keep the replay barrier busy; an estimate, not measured on hosted. As of 2026-11-13: F8 is closed by migration 00767 (primary club, session count, attendance rate and last session are not readable fromprofiles; they arrive through the masked profile functions, and the person's choice is the "주 클럽과 출석 현황" row on 공개 범위 설정); its first look on a device is List 53 H1 and H2. F12 is closed on the build side (the keys are out of every profile) and on hosted (the password was rotated). F14 (code-signed updates) is prepared, not switched on: the steps are in the pre-build checklist, step 8, and a build without it is acceptable for the test campaign; it joins the pre-launch list. F15 and the other items keep their reasons above.S38 (2026-11-13, closed in the fix round): a post reaction could not be switched, and any signed-in person could react to any post. Found while checking the grants 00764 kept:
club_post_reactionshad an INSERT and a DELETE policy and no UPDATE policy, and the adapter'ssetReactionupserts on(post_id, user_id), so a first reaction worked and changing it (like to fire) conflicted and was refused by row-level security (reproduced on the local database); the optimistic swap on the 게시판 reverted. The INSERT policy checked onlyuser_id, so the club was never asked. 00765 section 14: an UPDATE policy for the person's own row, and both write policies now ask for membership of the post's club (the read policy's predicate); the key guard now holdspost_idas well asuser_id(it helduser_idonly, so an update could have moved a reaction to another post and drifted the counts).offer_and_reaction_closurereplays the upsert statement PostgREST sends (a switch leavesreaction_countunchanged; a non-member's insert, a member who left, another person's row and a move are refused). The unbacked-grant ratchet is one (dues). Also: three adapter methods with no caller (seasonRecords.upsert,playerMatchups.upsertFromMatch,closePoll), two progression upserts and the venue correctionsubmit(a free-form insert that has had no INSERT grant or policy since 00409; no page or hook reached it) are deleted with their ports.S39 (2026-11-13, engineering pass wave 5): server capability with no screen. Eighteen mutation hooks that no page pressed were deleted from the app package (their ports, adapter methods and server objects stay). This is a list of features with a back end and no page, for the owner, not a defect: the live score call (call, agree, reject); a score correction proposal and its agreement; a referee's assignment, release and hand-over; a venue correction (submit, instant fact fix, review, report); a membership recommendation and its withdrawal; creating regular meets in bulk; starting the next round in bulk.
S40 (2026-11-13): UX-12 must clear
link-open-failure-toast.ts. A failed external link open has no page to stay on, so it is still a Toast with kinderror(the one file added to the lint ratchet in wave 5, with that reason). The UI/UX pass moves it to the opener's control; its lane clears the ratchet entry ineslint.ratchet.config.mjs. As of 2026-11-13: UX-12 moved the other loose error toasts to their pages, but left this one (link-open-failure-toast.ts, the baseline holds one): a link that will not open has no control to stay on and needs a decision. Open.S41 (2026-11-13, fix round): hosted reads owed after the push, and what the round could not close. (1)
end_sign_in_sessions_of_userrecords aninsufficient_privilegeto the sink and goes on (a withdrawal must not fail because sessions could not be ended); the sink is a private table nobody sees unless the operator reads it. After the push read whetherpostgresmay delete fromauth.sessionsandauth.refresh_tokenson hosted (select has_table_privilege('postgres', 'auth.sessions', 'DELETE'), has_table_privilege('postgres', 'auth.refresh_tokens', 'DELETE')); until that is read, F9's closure on hosted is unproven (the local role works). Answered (hosted, 2026-11-13):postgresholds DELETE onauth.sessionsandauth.refresh_tokens, so F9's closure holds on hosted. (2) The 가입 제안 word named the guest shortlist as well as the offer; resolved 2026-11-13 (batch 57): 가입 제안 is only the offer, the shortlist is 가입 후보 (terminology guide). (3)ingest-venuesno longer answers browsers (its CORS headers and OPTIONS branch are gone); nothing in the repo calls it from one. (4) A person's token not on the seed allow-list answers 401, as before (the round's brief said 403; the HEAD tests pin 401).S42 (2026-11-13, batch 57): owed after the UI/UX pass, wave A and B's first lanes. (1) Redeploy
send-push(two new push titles for the 가입 제안 notices; its copy file changed, no other function did). (2) The data export on the web downloads a.jsonfile through the share port's text-file path; a phone is unchanged. (3)fee-edit-modalshows one amount field where the board draws 무료 | 유료: carried to wave C's sessions lane. (4)global-error.tsxtheming and the web framessc-joinandkk-*are not built (no web image exists). (5) The Maestro flows 02, 08, 09 and 10 name things that are gone (02 asserts 탐색 and 클럽 만들기 on the clubs tab, which is now 클럽 찾기 with 클럽 만들기 inside the FAB menu; 08, 09 and 10 name the 탐색 tab text,club.my.card,club.discover.cardand the join button): they are rewritten in the readiness pass (every flow is blocked at sign-in today); they were not edited. (6) The DM members sheet's inline rename (participants-modal.tsx) discards typed input on a scrim press; it is outside the audit's six guarded sheets and goes to the next batch's clubs-and-leftovers lane. (7) The clubs family of pages still opens its own state spelling; the state helper reaches it in the next batch.head-to-head's opponent not-found needs a new read. (8) The boardflat-v2-list-ahas no frame for the guest roster's offered row (the roster is on the v1 boardguest-roster): add one when the roster is drawn on a flat v2 board. (9) No path checks blocks between a person and an official before an offer (nominate_for_membership,confirm_guest_conversion, the answer): pre-existing for nominations, carried to the readiness pass's privacy lane with the other block rules. (10) Fix round, what could not be closed:yarn qa:seed --localdid not run (first error line:seed-scenario is not being served; the local functions server is not up); nothing in the batch touches a seed (no seed writesadmin_nominated). The rendered check (dark, 200%, every page against its board) has not run for the fix round's changes. (11) Rendered-check list for the kit's role moves:AddCardlabel was 14/600 and is nowfoot13/500 (no flat role is 14/600),NaverBadgewas 11/700 and is nowcount11/700 (the glyph, not a digit); every other role move is a 1px size change that matches the board's role table and has not been seen. (12) Board grounding after the fix round: a board source with no recorded baseline is now a build error (scripts/lib/wireframe-grounding.mjs, canary testwireframe-grounding.test.mjs); the three that had none were recorded (messaging 2, courts 1).courtswas re-accepted by name (the venue page's state plumbing only;courts.jsxdraws none of those states),flat-v2-hubandflat-v2-list-aafter their last edits (the full club's offer band; the answer in the who row, the official's total Tag, the unread Count tag, the single-district sheet, and the new frameclub-official). The boards that already warned at HEAD were NOT re-accepted; they wait for the rendered check, where every page is compared with its board. Warning now (21 boards): home 2 (1 added by this batch); club-detail 6 (1 added by this batch); create-club 5 (0 added by this batch); web-app-2 6 (1 added by this batch); flat-v2-list-b 10 (3 added by this batch); flat-v2-detail 23 (12 added by this batch); flat-v2-auth 6 (1 added by this batch); flat-v2-forms 28 (12 added by this batch); flat-v2-flows 5 (3 added by this batch); records 8 (5 added by this batch); profile 6 (2 added by this batch); session-detail 13 (6 added by this batch); create-session 13 (0 added by this batch); live-scoring 1 (0 added by this batch); head-to-head 1 (0 added by this batch); settings 5 (0 added by this batch); social 3 (0 added by this batch); messaging 3 (0 added by this batch); onboarding 8 (1 added by this batch); legal 1 (0 added by this batch). Drifted by this batch alone: activity 2 (activity: the filter words moved into the app with the same words, andsession-card-v2draws kit icons where the card drew emoji; the board's card was not redrawn).S43 (2026-11-13, readiness pass): what the pass carries, with owners. (1) The retry rule: a read retries once on a transport failure only; a write is never re-sent unless it carries an idempotency key (
useQueueableMutation); a request that needs a signed-in person is refused before it is sent when the session is missing (SignInUnavailableError); the anonymous allow-list holds five functions and is pinned by tests; the app says offline only after three network failures and a failed health probe. Lane NET reported five script-runner tests that assumed the old rule; the integration ruled that a keyed write retries once on a transport failure or a 5xx (never on a 4xx) and corrected the five fixtures (last changed inbc9b90f7c; the rule is in31e15ec72):queueable-wrapper(its failing call is now a transport error,TypeErrorwithNETWORK_ERROR),venue-owner-client-wiring(the call goes through an allow-listed function),deferred-producers(the failure is a 503),sdk-producers(a 403 is sent once; the keyed 5xx repeat is held by the direct-message test) andquery-control(a failed read raises no toast). Todayyarn test:queue-integrityandyarn test:scriptspass on them (the batch 58 integration report records 223 and 1746 tests). Not measured on a device: the 20 s read deadline on a slow link, the web health probe behind a captive portal, the fallback on a real expired-token resume. (2)ingest-venuesstill has no 5 s deadline on its Overpass call (the file is undersupabase/, outside the network lane): owed. (3) Closed in the integration:scripts/prepare-r1-share-deep-link-flow.mjsnow signs in withQA_EMAILandQA_PASSWORDfrom the environment (no developer variable is read); its generated flow is still produced by that script (the EAS workflows run it), socheck-r1-evidence-workflowkeeps naming it. (4) Closed in the integration: the threepackages/test-kitdevDependencies are declared (yarn.lock+3 lines);scripts/check-maestro-flows.mjsis incheck:static(check:maestro-flows);hosted-ready.test.mjsis intest:scripts. (5) The Android splash has no mark image (the mark is a placeholder); the owner decides the mark splash. (6) A fresh read of the boards that still differ from their sources waits for the rendered check: 29 at that time, 27 after batch 59 re-accepted 22 of them by name (see S44 (6)); 28 by the build's own count after batch 60 (S45 (6)). (7) Closed:npx expo install --checkexited 1 on 2026-11-13 (eleven Expo packages one patch behind the table); batch 58 applied the updates and the check exits 0 on 2026-11-14. The native fingerprint now (Androidb7622917, iOScb73263e) is in the pre-build checklist, step 4.S44 (2026-11-14, batch 59 integration): what the colour, type, ink, seed, test and QA-document lanes leave for the rendered check. None of this has been seen on a screen. (1) Colour, every visible change (N1): the library's medal set (gold
#D9A00F, silver#8E8E93, bronze#B2580B, platinum#4A6378; glyph white on bronze and platinum, ink on gold and silver); a separate dark success fill ($successFill#00796A) for dots and check glyphs, the success text stays#75D1C6; one amber ($warningText/$warningFill); selected chips, style rows, venue pick rows and the filter button: green border, glyph and tint become an ink border and glyph on$gray100; progress bars, meters and the recruitment meter: green or state-coloured fill becomes ink on a grey track (the status word carries the state); the pill-nav active chip is ink with an inverse label; focus rings (web andStyledInput) are ink, not green; links in posts are ink and underlined; the web nav rail, sidebar and bottom bar active item is ink on grey (and 700); the saved-bookmark glyph, copy-value text, non-court venue pill, venue monogram and club-logo placeholder are ink on grey; the scoreboard tug bars and favoured label use the chart series blue and clay; the legacy primary Button is the ball colour with an ink label (the only two surfaces that still draw it: the crash screen's retry inerror-boundary.tsxand the wizard reveal's continue inreveal-stack.tsx; before: green fill and a white label); the donut chart's default segments are the series colours; confetti uses theme roles; photo scrims and the map dim are the ink#0D100Einstead of pure black (imperceptible); the web status page uses tokens. Check in dark and at 200%: pill-nav label contrast, ink on$gray100selected rows, the ink focus ring on a ball button (it is drawn outside the control; measured against the canvas, not the ball), the dark platinum tile. Two values moved that no live theme reads: the last-resort fallback table inpackages/ui/src/theme-hex.ts(THEME_HEX_FALLBACK) now equals the S2 light theme (the canvas and secondary surface#FFFFFFwhere they were#F8FBF9, and the status colours), held bythemes.test.ts; and the design system's--warningindocs/twomore_design_system/colors_and_type.cssis the darker amber (#6b4a00, was#946200), so the older v1 boards and the preview pages that readvar(--warning)draw it darker. (2) Type, every visible change (N2): the default text role isbody15/22 (was 14/22 uncapped); Badgexs10/14 to 12/16 (the pill grows about 2 px: club session card day tag, scoreboard draw and guest tags),sm12/18,md14/20 to 13/18; thecountrole 11/16 to 12/16 · 700 by the lead's ruling (the count badge's pill keeps its height); scoreboard tiebreak superscript 9/12 to 12/18 · 500 (each score cell 6 px wider); counters (recruitment count, hourly temperatures, filter count, dev panel) 11/16 · 700 to 12/18 · 500; pill-nav chip label 14 to 13; scope-tab label 14/600 (active 800) to 15/22 · 600 (active 700), cap 1.3 (check on a 320 wide frame at 200%); crash screen error line 14 to 15 and stack dump 12/18 400 to 500; linked text in posts and DMs takes the post's role (16 for reading roles) instead of 14; the web nav wordmark 800 to 700; status page error lines 14 to 13/20; initials in club logo bubbles and flag monograms no longer grow with the system text size;displayandcountdigits are tabular. Match cards on 기록 (the compact shelf card): the title and meta rows hold people and now wrap to two lines (the date line stays one); the card grows a line, the score stays at the right. To look at at 200%: PersonRow and MediaRow with a long Hangul name and a trailing Tag; the block hub (display 40 at cap 1.5); the score cell; the scope tab; the wizard back pill; the iOS tab-bar label;ErrorBoundary. (3) Web: the font is one unsubset 2 MB variable file (apps/web/public/fonts/PretendardVariable.woff2, 2,057,688 bytes) loaded under two family names with no metric-matched fallback: a performance item (a Korean face needs a dynamic subset), not built. The web sets its light or dark scheme before first paint (apps/web/lib/theme-boot.ts); Tamagui still server-renders the light theme class until hydration, so surfaces inside the page (cards) can flash light on a dark preference while the canvas, frame, scrollbars and browser chrome do not: check in a real browser. For the rendered check: thetheme-colortag the boot script rewrites (it finds no tag if Next emits the viewport metas after the script in<head>;ThemeColorSynccorrects it after hydration, so look at the browser chrome colour before hydration); states told by ink alone, to look at in light, dark and for a person who cannot tell the ink apart: the active PillNav chip (a$textfill with a$textInverselabel),MiniBarMeter(filled against empty bars),ProgressBar(ink on$gray100; the recruitment colour was removed), the donut series, selected rows; and whether a body link on the web shows an underline (apps/web/app/globals.csssetsa { color: inherit }and no underline rule was found, so a link in running text may read as plain text). (4) Closed by batch 60 (S45 (4)): was open for the owner: one pair failed its library threshold and is the only entry inscripts/color-contrast-baseline.json: dark platinum#4A6378on the dark card is 2.64:1 (dE76 30.3 where 36 is needed so that an earned tile never looks locked). The recommendation: a lighter dark-mode platinum, which changes the library's medal set. Two dark pairs sit at 6.00:1 and are gated at 4.5 (an inactive control; a 15 px Notice title): accepted, reason in the pair file. (5) Accessibility gap: iOSdynamicTypeRampand the Large Content Viewer are not implemented (library 2.4 against the code): a gap for the accessibility pass after the build. The mobile fonts are registered twice on purpose: theexpo-fontplugin embeds the four files in the binary (available at the first frame),useFontsis the load and failure gate and loads from the bundle where they are not embedded (web, an update onto an older binary). (6) Boards: 22 boards were re-accepted by name after their frames were read against the kit and the code (components, flat-v2, flat-v2-lib-chrome, -controls, -overlays, -social, -behaviour, -surfaces, flat-v2-match-row, flat-v2-brand, flat-v2-hub, flat-v2-list-a, flat-v2-list-b, flat-v2-detail, flat-v2-forms, records, session-detail, live-scoring, head-to-head, onboarding, courts, create-club); every key whose source this batch did not change was put back to its HEAD value. 27 boards still differ and wait for the rendered check: home, activity, club-detail, web-app-2 and flat-v2-lib-rows (their frames were not re-read:session-card-v2draws kit icons where the board's card drew emoji), and create-club, flat-v2-list-b, flat-v2-detail, flat-v2-forms, flat-v2-auth, flat-v2-flows, flat-v2-brand, flat-v2-lib-chrome, -controls, -overlays, -social, -behaviour, records, profile, session-detail, create-session, live-scoring, settings, social, messaging, onboarding and legal for sources this batch did not touch. (Update, batch 60: the count of boards that still differ is 28 by the board build's own count, S45 (6); the 27 above was counted differently.) (7) Seeds and the secret: the seed account password is the function secretSEED_ACCOUNT_PASSWORD(seed-scenarioandsimulate-activityboth refuse without it; HTTP 503seed_account_password_missing). After the push, the lead sets the secret on hosted, redeploysseed-scenarioandsimulate-activity, and (only on the owner's word, a hosted data write) runs the keyseed_accounts_passwordonce so existing seeded accounts take it. Locally the small world (readiness_qa, 22 of 22 checks) was seeded on 2026-11-14; the large world was replaced by it.venue_contribution_ledgertest 47 now reads its own fixture venue's inbox;00684_payment_refund_actionscould not be made to deadlock beside a hammering cron sweep on the small world (6 runs), so it is unchanged. (8) Not run: every phone flow, every device check,scripts/qa/hosted-ready.mjs --hosted --run(hosted), the signed-update steps (from Expo's documentation, not exercised), the maestro binary's own syntax check (not installed here).S45 (2026-11-14, batch 60 integration): the answers adopted from the research, built before the W5 build. Migrations
00768and00769are applied to the LOCAL database only; hosted stays at00767until the push. After the push the lead redeploysexport-user-data(it reads the new consent column) andseed-scenario(its excusal call passes the consent). None of this has been seen on a screen. (1) What a person sees, for the rendered check. N1 a block closes the other person's gated profile details and photo at every audience level, in a shared club too and on a public profile; an official's 가입 제안, guest-conversion offer or guest invitation to a person either has blocked is refused with 지금은 이 분께 보낼 수 없어요 (never the word 차단). N2 choosing 아픔·부상 on the 사유 인정 요청 Sheet (club and 매치 alike) shows an unticked consent row with who sees the reason and the 기타 way out; the soft 요청 보내기 says what is missing; the server refuses a health reason without it. N3 the comment box under a post and a chat's message box keep unsent text per post or thread until it is sent or the account changes (memory only; sign-out, a different account and a stopped provider empty it). N4 참석 where sessions are counted (streak, evening and morning sessions, n번째 참석), 테니스장 on every row that opens the venue finder (장소 stays for a club event and the 장소 상세 page), 경기 방식 with the line 친선: ELO 미반영, 경쟁: ELO 반영, and the empty 내 클럽 line that also points to the + button. N5 the split-settlement band's primary is soft and says 1인당 금액을 입력해 주세요 when pressed with no amount. N6 the member's 회비 pane leads with the oldest due charge (the recurring plan's on a tie). N7 a lighter platinum medal in dark mode; the Android adaptive icon's background is#15181A. N8 the 연결된 계정 row is gone from 설정 (the page L4-16 still waits for provider sign-in, the owner's decision of 2026-11-10; it is listed in the page inventory with that status). One more wording follows the guide: the deciders of an excusal are 운영진 everywhere, not 총무; for the rendered check, read the word in all 17 places that name them: the Sheet's club disclosure and 운영진 answer lines (disclosure.club,byOfficial), the 'rule applied' value on the record's Sheet, the trust-record help row for 사유 인정, the board frames and their specs, two screen-blueprint paragraphs, the library table, List 53, and the test names and comments. (2) What a blocked pair can still see of each other (a recorded limit, not fixed). Display name, ratings and tier, nationality, 구력, kata division and elite background (no audience gates them); club posts and comments; the club roster and a session's participant lists; group chat threads; an existing direct-message history; signals already queued. The block is silent and ends the friendship (00746); an offer already waiting is not touched (the person can still answer it, the official can cancel it). The pair for an offer is the acting official: an admin may nominate a person the owner has blocked. Cost of the extra probe was not measured (oneis_blockedper row). (3) The rating replay. Fixed for tied timestamps only: the history has a total order (00769), so equal timestamps no longer decide a streak rating and no longer force a full replay (the reasontied_history_orderis gone; seven fallback reasons remain; the 354 ms and 55 ms cases). NOT fixed: a correction of an old match on a large world still replays every later match (11.1 s and 15.3 s, cancelled at the API role's 8 s), and a large seeded world cannot be reconciled through the API (28.0 s; a privileged role runs the recompute once, 10.4 s). Proven bysupabase/tests/00744_windowed_rating_replay.test.sql(a tie across two matches no longer falls back; window and full replay agree on eight tables) and a streak test with a canary. Measured on the large local world after 00769 (S9): before the world is reconciled a correction still replays everything for the seeded history's own reasons and cannot finish under 8 s; after one reconcile, ties included, it takes the window path (354 ms for a tied player's match). Owed before launch: replaying only the players a correction can reach (an iterative closure from the corrected match's four players), for every path that can correct an old match (a review resolution up to seven days later, an exclusion, a reopening), not only the review, because such a correction still replays every match since then (about 4,900 matches at an estimated 700 a day for 100 clubs: 22 to 44 s from the recorded 4.6 to 9.0 ms a match, over the 8 s API limit; the window path measured 73 ms for one match); and the global per-minute replay cap (S37: about 45 colluding accounts could keep the exclusive replay lock busy; only the 10 an hour per account limit exists). A full replay extrapolates to 140 to 270 s at 30,000 matches. All estimates; hosted timing was never measured. (4) Dark platinum (closes S44 (4)).#56738Bmeasures 3.01:1 on the lightest dark ground (#232824, the locked tile), 3.39:1 on the card (#1A1E1B) and 3.79:1 on the darkest ground (#0F1210); the white glyph on it is 4.98:1.scripts/color-contrast-baseline.jsonholds no entry; its canary still fails as designed. 3.01 is the floor with no margin: look at the platinum medal on the darkest ground and on the card in dark for the rendered check. (5) Counsel reviews the health-consent wording before launch (excuse.sheet.healthConsentinko/excuse.tsanden/excuse.ts): the control, who sees the reason, and the way out. The columnhealth_consent_atrecords the moment. The rule is a trigger on the table (excuse_health_consent_guard): a new member-origin health row is refused without it, whoever writes it; a stored row is never judged, so a row the old function wrote before the push can still be answered, reminded and undone, and the moment cannot be changed once written. (6) Boards. 17 boards were re-accepted by name after their frames were read against the code (create-club, create-session, flat-v2, flat-v2-brand, flat-v2-detail, flat-v2-flows, flat-v2-forms, flat-v2-hub, flat-v2-lib-behaviour, -chrome, -overlays, -social, flat-v2-list-b, onboarding, session-detail, settings, web-app-2);create-sessionwas corrected (테니스장, 경기 방식 and its ELO line) and the four consent frames onflat-v2-detailand the 테니스장 and consent frames onflat-v2-formswere read; every key whose source this batch did not change was put back to its earlier value. 28 boards still differ from their sources by the build's own count (29 before this batch by the same count; the 27 stated in S43 (6) and S44 (6) was counted differently). (7) Moved to before launch by the adoption (not built here): the two old venue-correction functions closed in one piece with the twelve test files that still exercise the venue rules through them (SCOPE-4); a withdrawn person's private messages and their attachments deleted at the day-30 scrub, with the choice on the withdrawal screen and the written text once counsel has answered (PRIV-7); the stricter write rule for a withdrawn account (S37); the global replay cap (above). (8) Deleted with the functions, after launch. The ten functions closed to signed-in users in 00769 (call_match,agree_to_call,reject_call,propose_score_correction,agree_to_correction,assign_referee,release_referee,propose_referee_transfer,accept_referee_transfer,decline_referee_transfer);private.check_auto_calland its trigger function (inert: the trigger and the 5-minute sweep are off); the transfer signal typematch_referee_transfer_proposedand its copysignal.matchup.refereeTransfer.*; the read side of score calls (get_session_detail_bundle's score calls, the session card'sscoreConfirmPending, and the adapter readsfindActiveBySession,findPendingConfirmSessionIds,findByMatch: with nothing creating a call they read nothing);update_live_match_scoreand its unused hookuseUpdateLiveMatchScore; the columnsmatches.referee_transfer_pending_toandreferee_transfer_initiated_at(the client no longer reads them). The generated types (generated.types.ts) still list the ten retired functions, because the generator ignores grants; they go when the functions are deleted after launch. Pending score calls on hosted are the lead's to read after the push; no data fix is written. (9) The 연결된 계정 row and page (L4-16). The row is not drawn, because it opened nothing; the page returns with provider sign-in. (10) Not run: every phone flow; any hosted step; the cost of the profile masking function with the extra probe; the rendered check of the soft split-settlement primary, the consent Sheet in dark and at 200% text, and the empty 내 클럽 page.S46 (2026-11-14, batch 61 integration): what the rendered check found, fixed once. Migrations
00770and00771are applied to the LOCAL database and recorded; hosted stays at00769until the lead'sdb push. Edge function to redeploy:seed-scenario. The native fingerprint is unchanged (Androidb7622917). (1) What a person sees, for the rendered check. N1 홈's 소식 loads. N2 선수별 전적, 테니스 여정 and 장부 open, and every loading strip looks as before. N3 a club, a suggestion or a scorecard that is not there says "찾을 수 없어요" with its way back; 클럽 홈's not-found keeps its back arrow. N4 date headings in the schedule lists and the 게스트 명단 block sit on the page margin. N5 letters on coloured tiles are readable in dark. N6 the header's date is never cut; the place gives way. N7 amounts read "15,000원" (English "15,000 KRW"). N8 경기 예정 opened offline shows the offline wall. N9 the web tab bar draws the phone's five icons. N10 words: 테니스장 찾기; 변경 and 탈퇴 on the two buttons; "지금은 이 분과 대화할 수 없어요" when a chat cannot be opened; 일정 보기 on the album's door and the trust sheet; four venue-add lines say 테니스장. N11 the calendar's short last week and the heat map sit on their columns; a notice bar's action sits on its midline. N13 일정 상세 reads the session's own hold length (money-facing: a held seat is no longer drawn as lapsed after 10 minutes). N14 values that a read left out: a club's cancellation-penalty switch, the 메시지 미리보기 switch, a member's no-show and late-cancel counts, club and venue currency, another person's region and district on friend rows, and a withdrawn member no longer reads as new. (2) What the rendered check is, and what it cannot show. Pages are photographed from the built web app against the local stack with the seeded world, light, dark and 200% text, beside their board frames. It shows no device, no native keyboard or gesture, no 200% text on a phone, and on the test machine no map pictures and no emoji. (3) Not held by a gate (named, per ruling L4). "Every single-row read distinguishes no row from a failed read" has no gate; the per-page tests and the type rule inclub-page-state.tshold what was found. The other RPC bundles' rows are guarded at part level only (the select-list gate covers the 61 registered_COLUMNSlists; two lists are read as local typed shapes and twelve files hold literal selects, listed for the lead). (4) Before launch, set on the hosted project (read 2026-11-14,hubs/b60/hosted-settings-2026-11-14.txt). Password minimum length is 6 with no leaked-password check;exp://192.168.*.*:8081is in the redirect allow-list; sign-up is open at the API; there is no captcha. (5) A face URL already loaded keeps working after a block because theavatarsbucket is public. Accepted for now, named. (6) Boards. 20 boards were re-accepted by name after their frames were read against the code (club-detail, components, create-club, create-session, flat-v2-detail, flat-v2-flows, flat-v2-forms, flat-v2-hub, flat-v2-lib-behaviour, flat-v2-lib-chrome, flat-v2-lib-overlays, flat-v2-lib-rows, flat-v2-lib-surfaces, flat-v2-list-a, flat-v2-list-b, onboarding, records, session-detail, settings, web-app-2); 25 boards by the build's own count still differ from sources this batch did not touch.mb-roster(1672, with the new 우선순위 head) is a height estimate;measure:board-heightsandverify:flat-v2-boardsneed a browser and were not run. (7) Open to the owner's veto (decision log). the password and withdrawal buttons read 변경하기 and 탈퇴하기 as the component library's section 1.19 rules (the lead's first ruling, the nouns 변경 and 탈퇴, was corrected in the fix round; the 하기 list of the house-style gate now follows the table); 친구 신청 stays on the profile of a person who blocked the viewer (a block is silent: the server refuses withfriend_request_blocked, 42501); a suggestion the person may not read answers as not found (the server never says which). (8) Test runs are memory-bounded: every jest config takes its workers frompackages/test-kit/jest-workers.js(four workers, recycled above 800 MB); the first full gate of this batch was stopped by the memory guard in the workspace tests at 9313 MB before the bound. (8a) Why the fingerprint did not move althoughapps/mobile/package.jsonandyarn.lock(native-sensitive paths) changed. The app lost its declaration of@tamagui/lucide-icons-2; no mobile source file imports it, andpackages/app(which does) still declares it at2.7.7, so the package still resolves from the workspace root for the app's imports through@twomore/app. It is pure JavaScript: its folder innode_modulesholdsdist,types,package.json, a licence and a readme, with noandroid,ios, podspec,expo-module.config.jsonorreact-native.config.js, so Expo autolinking never saw it and the Android fingerprint (b7622917for this source) is the same before and after the removal. The 2026-09-09 precedent (6bbf7070e) advancedapps/mobile/ota-compatibility-baseline.jsonunder the old dependency-manifest rule; that does not apply here: since 2026-09-30 the baseline records the fingerprint EAS stored for the installed build (2f488e4d, builde4583905), and the current source differs from it on purpose until the post-flat-v2 build (W5), so advancing it now would claim an installed binary carries source it does not. The baseline is untouched and OTA stays blocked by the guard. (8b) Not held by a gate (named). The select-list gate reads*_COLUMNSconstants and single-quoted one-line inline selects: a select written as a template literal or across lines is not seen. The list of functions that derive a uuid from a hash is kept by hand inscripts/lib/derived-uuid-functions.mjs; nothing ties it to the client'sz.guid()side, so a new derived id type needs both edited. (10) The fix round after the three reviews (2026-11-14). The seed changes only the seeded world: its UPDATEs and DELETEs state their scope throughseeded-scope.ts(seeded accounts, seeded clubs, rows written since the oldest seeded club was made), the developer account's rate counters are cleared only forfriend_requestanddm_create_group, and the platform-staff row is granted for the decisions and removed in afinally(a data-state check asserts no seeded account is a platform admin;scripts/__tests__/seed-sql-scope.test.mjsfails for an unscoped statement). The capture tool probes where the app signs in (a throwaway pair, the auth request held back) and refuses to type a password anywhere but the local API. The password and withdrawal buttons follow § 1.19.venue_refresh_applyand_outcomesclose claim-held venues as_ledgerdoes. Not run: the hosted project; the web build; any device. (9) Not run: any device; any hosted step; the web build (the lead rebuilds after the push); the board height measure.S47 (2026-11-14, batch 62 integration): 홈 with a match in play; a name is never blank; a drawing rule does not take the page. No migration. Edge functions to redeploy: none. The native fingerprint is unchanged. (1) What a person sees. N1 홈 no longer falls over for an account with a match in play: its match block shows its loading form until the players' names have loaded, and still draws, with the one rule's words, if that read failed. N2 a person's name is never blank: a withdrawn (anonymised) person reads 탈퇴한 멤버; a missing profile or a blank name reads 신규 멤버. N3 in a release build a drawing rule that fails leaves out its own component and reports it; in development and under jest it throws as before. The tab bar and a Notice keep their throw in every build. (2) Not held (named). The throws left under
packages/ui/src(programmer-only preconditions, a missing provider, an exhaustive switch,DetailShell's action count, the Notice and the tab bar) are on the list indesign-rule-throws.test.tsand can still take a page; the notice bar's action and the release-build behaviour are held by tests, not by a picture; the seed holds no withdrawn person in a played match, so no capture shows 탈퇴한 멤버 on a match row; nothing ran on a device; no canon rule says what a Lead block whose own read failed draws.S48 (2026-11-15, batch 63 integration): the lead's own review before the owner's. Migrations
00772_permission_probe_findings.sql,00773_session_series_accepts_challenge_tag.sqland00774_revoke_consent_refuses_required.sql(the last: a required consent is withdrawn only by leaving the service, L22; an optional one as before; the page shows the 필수 line on a refusal) (applied to the LOCAL database; the lead'sdb pushfollows the gate). 00773 is a fault the journeys found: since 00756 the client sendschallenge_idon every club session create butcreate_session_serieskept its fixed column list from 00753, so every club 일정 만들기 answered "Unsupported session field" (the column joins the list; the tag triggers still validate it). Edge functions to redeploy:seed-scenario. The native fingerprint is unchanged (b7622917; one manifest lost a dependency in this batch,packages/features/messaging/package.json(tamagui, which nothing there imports), which autolinking never saw). (1) What a person sees (N1 to N9). N1 who can read what: a match's set scores and reactions only through a match the person can see; a signed-out caller reads no session column; a blocked pair read the same leaderboard as a stranger; a chat that cannot be opened is refused in neutral words by the server too (dm_unavailable); the club media folder lists only to the club's officials; an unknown invite code is a refusal, not a server error. N2 at desktop width the tabs sit in the reading column and session rows are one column. N3 the chat and comment boxes share one visible field (ComposerField) and received bubbles, notices and other fills are visible against the page ($gray100;$surfaceSecondary, the page's own ground, is retired). N4 가입 신청 counts people who asked to join and a sent offer has its own heading, 보낸 가입 제안; a player's games on 선수별 전적 show the total with the per-side figure labelled; a waiting receipt has its own tone. N5 the empty ledger offers 시작 잔액 설정; every plan gate is one component with one sentence; not-found walls have the back arrow alone and their door; 동의 관리 lists the required consents as required (from the sign-up catalog, not the legal basis) and says they end only with 회원 탈퇴. N6 money in full ("20,000원", "월 30,000원", 시간당), a time ago with 전, a day for reading in the house form (feed rows and cards keep the boards' compact MM.DD (요일): the lead's ruling), a wait as days, hours and minutes, ratings with one decimal, counts with their unit. N7 the owner's table words (수준 무관, 미납 · 확인 대기 · 완납, 저장됨, 알림 보내기, 신청 취소, the verb with 하기 on the suggestion, claim and consent buttons, 운영진 on the no-permission walls, 홈's first-day line). N8 on the web the Tab key reaches every pressable and Enter or Space presses it; a bordered link row is 44 tall; the venue picker and the trust meter sit on the rail; a dialog takes focus inside itself; map thumbnails are decorative; a chat message's long press has a spoken name. N9 not visible: the QA world holds achievements, journey notes, trust scores, attendance strikes and required consents (38 accounts, 58 checks), and the review's two tools (the measuring mode and the journey runner ofscripts/screenshot-web.mjs). (2) How each was found. The lead's review ran four methods over the built web app and the local stack (the measurements and the journeys ran over the BUILD OF BATCH 62 (43bc37f94) against the local database with batch 63's migrations applied; batch 63's client changes (the friends-list refresh, the words, the keyboard reach, the composer, the reading column) and the journeys' fixes have NOT been seen in a browser: the lead re-runs the measuring mode and the journeys after batch 63 is built and records the result then): a second reading of every picture against its board; measurements from each rendered page (overflow, clipped text, rail, tap targets, contrast, broken images, missing accessible names, a dump of every visible sentence); permission probes through the data API as each kind of account; and journeys clicked through with the database read after each. Every finding was fact-checked against the code and ruled by the lead; a reader checked every sentence against the words table. (2b) The fix round after the three reviews (F1 to F14). A required consent now also refuses a client's directuser_consentwithdrawal row (triggertrg_user_consent_refuse_required_withdrawal, in 00774; account deletion runs as the function owner and still closes every consent); the friend-request refusal names no block (friend_request_unavailable, 00772; the chat'sdm_unavailablewas the first); Space and Enter press by role on the web; the 필수 head explains how a required consent ends through its?; cancelling a sent request is its own mutation (신청 취소, every list refreshed); one send rule for both composers; the registry follows the Words table (a test reads the table's refused column); the seed's local-only rule holds per call in every scenario folder;ConfirmProviderkeeps the options of a dialog shown within 250 ms of another closing. Edge function to redeploy:seed-scenario. Still unseen in a browser (see (2)). (2a) What the journeys found beyond 00773 (L23 to L28). The journeys (16 of 16 ended in a matching database assertion, over the build of batch 62 with batch 63's migrations applied: see (2)) found one must and five shoulds. Fixed: a club session could not be created at all (00773; since the commitf987770c2that made the client sendchallenge_id, becausecreate_session_serieskept its fixed list of 00753 and no test compared the client's payload keys with it:session-series-allow-list-parity.test.tsnow does); the friends list read 아직 친구가 없어요 after an accept (the request row and the list are now refreshed together and the mutation waits for both); a notification said "strike" (불참, with a house-style rule). Read and found right: a score review asked on a live match is not offered to the staff member who plays that match (scorecard-operator.rules.ts, ruling O1: the journey's official had entered the score as a player), and the 건강 사유 without the tick does show its line (excuse-request-sheet.test.tsx, "without the tick the press sends nothing and names the missing consent"); the wall's 클럽으로 door replaces to the 클럽 tab (the journey pressed the header's 돌아가기, which is history back). Seen and not explained: a press on 참석 취소 within about three seconds of joining did nothing in the browser (the push prompt Dialog closes right before; the journey waits three seconds). Batch 63's fix round (F13) found by reading a REAL fault that fits the scene and repaired it, and it is NOT proven to be the three-second one:ConfirmProvideremptied its options 250 ms after ANY close with a timer thatshownever cancelled, so a confirm opened inside that window mounted, lost its options and drew an empty scrim (fixed bypackages/ui/src/deferred-clear.ts, tested with fake timers); the window is 250 ms, not three seconds, so the lead traces the journey in a browser after the build, and the cause may still be the push prompt's exit keeping the page inert; the host's 참석자 관리 list showed the payer under neither 미납 nor 참석 확정 for a moment after 입금 확인 (a transient refetch);/create-session?clubId=opened the chooser on a fresh seed and the seven-step wizard on later visits of the same world. (3) Not held (named).venue_source_observationsis still readable by every signed-in person (the venue page reads it directly throughgetSourceValues): pre-launch, an RPC that returns only the fields the page shows, then the table closed to clients (ruling L2). A region-filtered leaderboard still drops a blocked person, because their region field is masked for the viewer (L3: the region board's rule for whose region counts is a privacy question of its own). 71 otherP0002sites in 55 functions answer as HTTP 500 though the client reads the hint; monitoring will count refusals as server errors (L4; the list ishubs/b63/SRV-p0002-sites.txt).process_seed_achievementsawards to every club member and takes no argument; the scenario calls it only against the local stack, and scoping it by an argument is a migration for later. Achievements are awarded on the player's device (an award a client can write is an award a client can forge: acceptable for badges shown to others? a pre-launch question, L19). A withdrawn member who played is still not in the QA world, so no picture shows 탈퇴한 멤버 on a match row (private.anonymize_withdrawn_profiles()sweeps every profile and has no per-profile form; L18). The 35-odd design throws listed by batch 62's scan are still on the list indesign-rule-throws.test.ts. Seven older boards (components,create-club,create-session,activity,profile,session-detail,onboarding) were not re-read against their sources and keep their grounding warning. The month grid (Sunday first) against the week strip (Monday first) on 기록 is left as it is: a question for the owner (L6). Nothing ran on a device.S49 (2026-11-15, batch 64 integration): the final run of the lead's own review, built. Migration
00775_dm_thread_activity_order.sql(dm_threads.activity_at, a stored generated column: the last message's time, else the creation; the chat list orders and pages on it) is applied to the LOCAL database and recorded by the CLI (the lead'sdb pushfollows the gate). Edge functions to redeploy: none. The native fingerprint is unchanged (b7622917;yarn install --immutablepasses). (a) What the re-run after batch 63 SAW of batch 63 in the built web app on 2026-11-15 (this answers the sentence in S48 that batch 63's client changes were not seen in a browser; S48 is not edited). 334 of 340 pictures showed their state; the six others are two out-of-date expectations of the tool (L1-05/ideal,L3-46/ideal: both pages right). Measurements: keyboard reach was at fault on 86 page states before batch 63 and on none after; the 42 px link rows are gone; the remaining hits were ruled (the live ring, count marks on header icons, text inputs inside taller cells, chat bubbles, dimmed calendar days). Journeys: 16 of 19 passed; the three that did not are F1 (a real fault batch 63's fix did not fix: the friends list did not refresh after an answer), F2 (a real fault: the push dialog opened in the browser) and a wrong step in the lead's own journey. The permission probes were re-run from outside: 1,029 probes, each row found by query, no new finding; one difference is kept by batch 63's ruling (a signed-in stranger readscreated_by,cost_totalandmatch_rulesof a public pickup). Edge functions, deletes and rate limits were not probed. (b) What batch 64 changes, and how each was found (the journeys, the measurements, a third reading of every picture, the fact-check of every finding against the code, two reviews of the built diff). N1 lists that refresh and read in order: an answered friend request shows on 친구 목록 at once (the key factory's short form is now the prefix of the long one, with a behavioural test and a tripwire scan for the literal filler pattern); 메시지 lists chats by their last message (00775); a person's region reads 서울, not the stored key (the row model owns the label). N2 a person's record: 총 경기 on 기록 counts the three rated tracks (it summed two; rated mixed-doubles matches now count toward every figure and stage on 기록 built on the rated total, which the same fault had left out). N3 the browser: no push dialog where there is no push ('unavailable'); the ink ring only for keyboard focus (the scan covers ui, features, the app's presentation layer and the apps, with a canary for the multi-line form); an empty composer is one line high and grows to its cap (useAutoGrow, tested on a fake node). N4 marks that hid things: a wide count on a header icon grows outward; one mark per corner of the club photo (the counter with two photos or more, the 사진 door with exactly one: with two or more the hero has no control to the album, which the journey feed and the album list still reach: decision log R6); an offer row's tag sits on its second line; the account's own letter tile is a capital (one helper,personLabel, hands every row the printed name and the face name; a scan refuses any other builder of the viewer mark). N5 drafts: opening 일정 만들기 or 클럽 만들기 saves nothing; a draft exists once the person changes an answer or goes to the rules page (the door saves it first). N6 small faults: achievement tiles draw their symbol; an empty roster on the preview says so; a lone history row is not drawn without its head; the dashed add card's edge in dark; the seam under the club row on 공개 프로필; 친구 목록's first heading spacing; the growth page's figures read 회 over event counts (the funnel counts rows ofclub_growth_events, not people) and its source heading reads 활동이 많은 경로. N7 words: 참석 for "RSVP" in Korean sentences (the house-style gate refuses the Latin word); "N명 초대하기"; a pending 제안 상세 with one main action. N8 not visible: query-key factories, the capture tool'sfillstep, three steps of the phone checklist. (c) What is NOT held. Batch 64's client changes have not been seen in a browser at the time of writing (the capture run after the push is the check). Nothing has run on a device. Text at 200%, map pictures and emoji cannot be drawn on this machine. The board frames corrected by this batch were read against the code; the 18 boards whose sources changed were re-accepted by name on that reading (the other changed sources are focus-style, label-helper and copy edits); no browser measure was run, so the heights of the frames added or changed are estimates. R15 (decided, not built, before launch): 총 경기 reads the rated counters on a person's own 기록, 선수별 전적 and the host card (40 for the QA account), and every completed match with a score on the public profile (78); the ruling is that 총 경기 and 승률 mean every completed match with a score, which needs the own pages' own read path. O1 (dark soft fills), O2 (achievement tier colours) and O3 (desktop rail word) are open owner questions. Open notes of the lane: the seeded expired row on 동의 내역 has no expiry date (seed data; the code draws the date when it is present); the native notification adapter still answers'undetermined'on a simulator (a test pins it); the club-photo hero has no direct control to the album with two or more photos (R6); the composer's real heights and the dark band behind it, and the ring's whole outline on the tab scroller, are the browser's to show. The pre-launch items of S48 stand.S50 (2026-11-15, batch 65 integration): a Toast never sits on a page's bottom button. No migration, no edge function; the native fingerprint is unchanged (
b7622917;yarn install --immutablepasses). (a) What was found and built. Journey J17 (the timed variants) pressed 참석 취소 at once after 참석하기: the Toast "참석 확정됐어요" sat on the button, and a Toast with no action dismisses on tap, so the press did nothing. The cause: when the band primitive replacedBottomCtaBand, the reporting of its height to the bottom-chrome store was lost (the oldonHeightChangewiring was by hand at a few sites, never at the 41 and more bands). Now every bottom-pinned primitive reports itself through one seam (CtaBand, a placedFab,PinnedBottomBarfor the two composers; the app mounts the reporter once per root); a layer counts only while its screen is focused; the two tab layouts report the bar through the same reporter; a band docked in a supporting pane (840 and up) reports 0. The wizard footer hook, the band hook and theonHeightChangeprop are deleted, and so is thecta-band-swapcodemod (noBottomCtaBandis left to swap). A scan test with a canary fails on a bottom-pinned file that neither reports nor is listed. (b) Only a phone shows (List 53, step D2b). Join a free session and press 참석 취소 at once: the message sits above the bottom button and the press opens its dialog; the same on a wizard step (above 다음) and on a tab page with a FAB (above the FAB); after a page opens over a tab, a message sits 16 above that page's own button, not above the tab bar's height. The native stacking, and that the tab layout is blurred under a pushed page, cannot be checked in jest or on the web. (c) Recorded, not built (NAV-10, decided on the phone after the build). With the keyboard up the Toast sits at the keyboard's top (toastPintakes the taller of the chrome and the keyboard), so it covers a composer that rides on the keys. Look at a Toast while the keyboard is up on 채팅 and on 게시글 (List 53, step D2b).S51 (2026-11-15, batch 66 integration): the owner's four marks, and the failed first build. Migrations
00776_player_results_rated_only.sql,00777_recommendable_guests_blocked_by_candidate.sqland00778_ranking_row_facts_rated_only.sqlare applied to the LOCAL database and recorded; the lead'sdb pushfollows the gate. Edge functions to redeploy: none. The native fingerprint moved on purpose, by the lead's splash fix only: Androidd51e2ac2(fulld51e2ac21438e87a0199546931d764afdfc9a051, with the build service's variables), iOS63971091;yarn install --immutablepasses. (a) The failed build. EAS build4a0f7492-a5f3-4da0-9739-9273e3cce534(sourceceae2ecfd, fingerprintb7622917) failed after 29 minutes at:app:processReleaseResourceswithresource drawable/splashscreen_logo not found. Cause, read from the build log and the installed package:expo-splash-screen's plugin always writes a theme pointing at that drawable (withAndroidSplashStyles.js:45) but writes the drawable only when an image is named (withAndroidSplashImages.js); the config sincec1d2c2dbdhad colours only. Fix: the plugin names./assets/splash-empty.png(288 x 288, fully transparent), so the start screen stays the plain white or near-black one;scripts/lib/splash-config.mjsand its test (5 tests, one a canary for the failed configuration) hold it. Proof: the plugin's own image step, run against a temporary folder, writessplashscreen_logo.pngat five densities with the new config and nothing with the old one. The real proof is the next build; the mark splash stays the owner's decision (item "(5) The Android splash has no mark image"). (b) Verified by test. Dark soft fills: the contrast gate holds every soft fill at 1.5:1 on the page and the card, the panel edges and the Notice tint, with a canary that goes red on the old fills (368 pairs, 0 fail); light thresholds untouched. Tiers: the ornament geometry (frames, star counts, sizes, pitch) and the locked outline are pinned inthumb-visual.test.ts; the kit mirrors the app under a parity test. Counts: pgTAP for 00776 (one population per call, the old four-argument call still resolves and returns its old keys, a walkover and a friendly stay out of the rated record, the rated count equals the rating counters) and 00778; jest for the tiles, the own split (kind-rows) and the club peek. Blocks: pgTAP for 00777 (7 tests) and 00768 still passing; jest for the row states and for the refusal dialog (a plain string and an unknown failure render as before). (c) What only a browser picture will show (the lead takes pictures after the push). The two ladder framesac-ladderandac-ladder-dark, the 가입 후보 blocked framemb-blocked-candidate, and every board that reads the kit, in dark and at 200%. (d) What only a phone shows (List 53, steps A6b, G2b, H2b, H5). Dark fills on a real screen (the soft button on an unfinished form, letter avatars, a grey notice, a dialog's edge); the tile ornament at phone density; the block row and the refusal with two phones. Recorded, not verified: rating reversals decrement counters, so the rated count against the counters is not checked on the seeded or hosted world. (e) Recorded, not built. Telling an official that a particular member blocked them (decision log S4). A neutral Notice tint on a card cannot reach 1.5:1 without its title falling below 4.5:1 (styling canon).S52 (2026-11-15, batch 67 integration): the second W5 build fails the boot-smoke, and the phone's compiler is now in the evidence. The second build (EAS
0c226ef8-08bc-4275-8ad0-66da438b5b7b, sourcea99d0142e, fingerprintd51e2ac2, version 0.12.0) finished; on the emulator it installs and the start screen is plain white (the splash fix works), then the sign-in page shows 문제가 발생했어요 with "Should have a queue. You are likely calling Hooks conditionally". (a) Cause and fix. The phone build compiles every file with the React Compiler (experiments.reactCompiler: true); jest and the web build did not.useBottomChromeReportcalled the hook it reads from context asuseContext(...)(), which the compiler cached as an ordinary function, so every page with a bottom-pinned primitive threw on its second render. The provided hook is now bound to ausename before it is called; proven by compiling the file withbabel-plugin-react-compiler(the old form holds the call inside the cache guard). (b) Verified by test.packages/ui/src/__tests__/bottom-chrome-report-compiled.test.ts(3 tests; the canary is the faulty form and throws the device's error). The web build hasreactCompileron whenTWOMORE_WEB_REACT_COMPILER=1; the first compiled build held 2,594 uses of the cache sentinel against 2 uncompiled. The first compiled run (the seam already fixed) could not sign in at all: every one of the 342 page states and all nineteen journeys stopped at the sign-in page with React error 300 (rendered fewer hooks). Pausing a headless browser on the exception in a build with readable names gave the component,LoginScreen:useLoginFormheld{ email: useRef(null), password: useRef(null) }, and the compiler caches the literal, so both hooks were skipped on the next render. With that fixed too, the second compiled run rendered all 342 page states (336 show exactly their state; the other 6 are the three known test-data differences) and passed all nineteen journeys.check:compiled-hooks(incheck:static; 3,579 files; seven script tests with both faulty forms as canaries) now refuses a hook call inside the compiler's cache. (c) Not verified. Native-only faults (the compiled web run is a browser); any page behind sign-in (the emulator was signed out); the compiled web app's speed and bundle size. The third build is made from this batch's commit with the same fingerprint (d51e2ac2: both fixes are JavaScript only); its id and its boot-smoke are recorded with the baseline step of the post-build checklist. Preview builds carry the one-tap developer sign-in again (the owner, 2026-11-15, decision log S6), so the boot-smoke can reach signed-in pages.
Evidence Entries
2026-11-13 - Batch 58: The UI/UX Pass Finished And Readiness Wave 1 (No Artifact Shipped)
- Change: the rest of the UI/UX pass (clubs page states, the tab re-tap, one date editor and fee form, words, small rules, 51 club-home frame tests) and the readiness pass's first wave (the network layer, native configuration for the next build, profile fields private by default, the server-health reader). Migration
00767_profile_fields_private_by_default.sqlis applied to the LOCAL database only; the hosted project is at 00766. No OTA, no build, no edge-function deploy from this batch. As of 2026-11-13: (5) closed in the code: the fourteen flows are rewritten, linted and not run. (7) closed by batch 58: every club page opens its states throughpageState/PageStateView/ClubGateView. (3) the fee fork was reported built by the UI/UX pass (wave C, lane D); not seen on a device. (10)yarn qa:seed --localand the rendered check are still owed after the readiness pass. - Privacy, for the owner's veto (hosted effect once 00767 is pushed). A person's primary club, attendance rate, session count and last session reach another person only through the
activityaudience (default 같은 클럽, which also reaches accepted friends). Before: every signed-in person saw all four on anyone's profile, the leaderboards and a pickup roster. After: a co-member and an accepted friend see exactly what they saw; a stranger (no shared active club, not a friend), a former clubmate and a signed-out visitor see none of the four (the page draws no club row, no attendance on a pickup host's card, no club label on a leaderboard row; the public share preview carries a session count of 0 that no page draws). The person can widen it to 전체 공개 on 설정 > 공개 범위 (the new row 주 클럽과 출석 현황). - Database ports, as measured. This PC has a public address; Docker Desktop publishes the local stack's ports on all interfaces and ignores the bridge's host-binding option; the Windows firewall's Docker rule has been off since 2026-11-10. One hardening command is the owner's (see device-setup.md, "Docker port binding").
- Developer account. Its password was rotated on the hosted project on 2026-11-13 and no build profile holds it (
apps/mobile/eas.jsonhas an emptyenvobject in all three profiles; F12 closed; the value in git history is dead).docs/guides/credentials.mdand the Maestro flow01-login.yamlstill describe the old developer sign-in (the readiness pass's DOCS and E2E lanes). - Set aside by the owner, 2026-11-13: the cockpit's login (EXT-20) and the brand name's filing and addresses (EXT-17, EXT-18);
docs/external-blockers.mdsays so without changing their facts. - Counsel and seeds. The legal text (
terms.ko.ts) keeps the word 번개 ("번개 찾기"): counsel's text, not edited here; it joins EXT-2 (counsel). The seed fixtures' 번개 goes with the readiness pass's seed lane. - Not built, recorded: the club home board's
club-coachframe (a one-time coachmark on the 관리 gear for a member who becomes an official) is drawn and built nowhere (Club Detail blueprint, Gaps). - Only a device can show (flows to try on the next build): (1) cold start in a light and in a dark system theme: a white / dark splash with no flash, then 홈; (2) 설정 > 화면 모드 다크 on a light phone: status-bar icons, keyboard and date picker turn dark; 라이트 on a dark phone: they turn light; 시스템 follows the phone; (3) 클럽 > 회비: 복사 beside the account number puts it on the clipboard (toast 계좌번호를 복사했어요); (4) adding a photo: iOS asks with the new Korean sentence, Android shows the system picker and no camera prompt, app info lists no Camera permission; (5) the location prompt reads the new sentence; (6)
adb shell dumpsys package com.ivorybridge.twomore | grep allowBackup; (7) Android 12+ push in light and dark shade: the accent reads on both; (8) re-tap each tab: it scrolls to the top, and from a deeper page returns to the tab's first page; (9) a scrim press or Android back on a Sheet with typed input asks before discarding; (10) a captive-portal Wi-Fi: after three requests that get no response, one health probe runs and, if it too gets no answer, the offline strip shows and stays until the real server answers (a slow server that still answers never shows it); (11) a forced crash in the root: the crash screen's content sits below the status bar and above the home indicator; (12) 일정 수정 on a session dated in the past: 날짜 > 완료 with no new day closes the sheet and changes nothing; (13) three photos uploading on a slow link while 홈 is open: 홈's reads still load. - Fix round after the three reviews (lead's rulings). Built: the database client's own read retry is switched off on every client the app builds (the app's one retry is the only retry); a deadline the app cut no longer counts as "the server did not answer" and three real failures ask ONE probe before the app says offline; the anonymous allow-list lost the viewer-dependent and the uncalled entries; failed reads refetch when the session renews or the server answers again; uploads take two slots of their own; a REST 429 is
rate-limited,status: 0istransport, a bareTypeErroris a defect; a write refused as sign-in unavailable is a breadcrumb; sign-out still clears the push token when only the server is silent; a declined or failed copy of the account number falls back to the share sheet; a past-dated session's date sheet closes on 완료 with no new day; a second press of 종료 and of 환불받았어요 sends one request; the 입금 완료, 조 and photo sheets are dirty only when the person changed something. Details:docs/canon/networking.md. - Update guard for the copy button (G1).
expo-clipboardis imported at the top of the app barrel, so a binary built before this batch fails at module load. An update (OTA) that carries this batch must never reach a binary built before it; the native fingerprint guard (apps/mobile/ota-compatibility-baseline.json, the OTA guard) refuses exactly that, because the batch changes the fingerprint. - Boards, as measured after the fix round (J1). The three boards this batch redrew (
flat-v2-hub,flat-v2-list-a,flat-v2-detail) were re-accepted only for the sources the batch changed (flat-v2-detailkeeps its 19 older drifted sources, which nobody has rendered; they are back at their earlier baseline). The build reports 29 boards that still differ from their grounded sources (source counts): flat-v2-forms 37, flat-v2-detail 19, session-detail 15, create-session 13, flat-v2-list-b 11, club-detail 9, records 8, onboarding 8, web-app-2 7, profile 6, flat-v2-flows 6, flat-v2-auth 6, settings 5, create-club 5, social 3, messaging 3, home 2, activity 2, flat-v2-lib-overlays 2, flat-v2-lib-chrome 2, flat-v2-brand 2, live-scoring 1, legal 1, head-to-head 1, flat-v2-lib-social 1, flat-v2-lib-rows 1, flat-v2-lib-controls 1, flat-v2-lib-behaviour 1, components 1. They wait for the rendered check, where every page is compared with its board. - For the owner, effects of the privacy default (veto or accept). (a) A blocked pair who still share an active club keep seeing each other's profile activity and faces: the audience rule has no block clause (faces behaved so before); it is carried with the other block rules to the privacy questions the owner decides. (b) On a public 매치, a stranger no longer sees the host's club or attendance rate unless the host chose public (the consequence of private-by-default); the pickup roster's club chips go dark for strangers the same way.
- Native packages recorded in the knip baseline (J2).
expo-clipboardandexpo-file-systemare declared at the app level for Expo autolinking, which knip cannot see; each has a reasoned entry inscripts/knip-baseline.json(82 entries at HEAD, 83 now) instead of a blanket ignore inknip.json.
2026-10-14 - Preview Build 0.12.0 Attempts And The Preview OTA Pause
- Change: the user-facing brand renamed to Dabusi / 다붓이 (
4ce47469: display name, icon set, in-app logo, web metadata; the native fingerprint changed), the Expo SDK 57 patch alignment with the app bump0.11.0to0.12.0(540edb91), per-platform keys in the Expo locale files (a9dcab26), and Pretendard in the web OG images (08f65cdf, web only). - Classification: preview-build, one failed attempt and one build not adopted; no OTA.
- Source commit:
540edb91carries the version bump;a9dcab26fixes the failed attempt. The source commit of buildbae319b1is not recorded in the docs. - Source sync: not recorded.
- Source sync proof: not applicable; no OTA ran.
- Target: preview build.
- App version:
0.12.0(from0.11.0). - Runtime version:
0.12.0(runtimeVersion.policy = appVersion). - EAS artifact: Android preview build
61e9fde1failed atlintVitalRelease(ExtraTranslation: iOSCFBundleDisplayNamereached Androidstrings.xmlthrough top-level keys inapps/mobile/locales/*.json). Buildbae319b1(0.12.0) exists and is not adopted. - Backend artifact: none.
- Verification:
expo install --checkhad failed on nine Expo packages andexpo install --fixaligned them (540edb91);scripts/__tests__/mobile-locales.test.mjsnow rejects top-level keys in the locale files (a9dcab26). - Smoke evidence: none recorded.
- Monitoring checked: not applicable.
- Rollback pointer: the baseline was not advanced;
apps/mobile/ota-compatibility-baseline.jsonstill names the 0.11.0 builde4583905. - Known gaps: owner decision, 2026-10-14: the next native build waits for the flat v2 migration, ships with the new mark (W5) and becomes the new baseline; preview OTA stays paused behind the fingerprint guard until then. The repo docs record no source commit, fingerprint or smoke for
bae319b1. - Source detail: decision log ("Owner decision — the build waits for flat v2"), rebuild log (the 2026-10-14 entries), migration plan (Build waves).
2026-10-06 - Codex Landing: Backend 00678-00690, Five Edge Functions And Preview OTA 1719d507
- Change: the Codex audit landing (draft PR #15, landed by lane through the stack
56a2d261, #26) and migration00690_cron_prelocks_session_roster.sql(420865f7): privacy, payments, messaging, push and club repairs, with their client counterparts. - Classification: backend-deploy plus OTA-safe client source.
- Source commit: the OTA source commit is not recorded in the docs.
- Source sync: not recorded.
- Source sync proof: not recorded.
- Target: backend deploy and preview OTA.
- App version:
0.11.0. - Runtime version:
0.11.0. - EAS artifact: preview update group
1719d507(short id as recorded in device List 52; the full group id is not recorded). - Backend artifact: migrations 00678-00690 applied to the hosted QA project (hosted DB at
00690); edge functionssend-push,check-push-receipts,seed-scenario,fetch-holidaysandexport-user-datadeployed. Deploy order in the decision log: migrations, then the five functions, then OTA. - Verification: the per-lane checks are in the decision log's CODEX LANDING entries (D3, E, F and STACK);
00690has pgTAP00690_cron_prelocks. - Smoke evidence: device List 52 (ten rows: seven for the phone, three for the web app in a desktop browser); no hosted Maestro run is recorded.
- Monitoring checked: not recorded.
- Rollback pointer: not recorded.
- Known gaps: the owner's install smoke of the 0.11.0 build (List 47) was still open on 2026-10-06, and every later OTA list (48-52) assumes it.
- Source detail: Device checklists (List 52), decision log (the CODEX LANDING entries and DOCS AND WAIVERS SWEEP).
2026-09-30 - Preview Build 0.11.0 e4583905 And The Fingerprint OTA Guard
- Change: dependency upgrade Phase C as a new native build, runtime
0.10.0to0.11.0: react-native-mmkv 4.3.2 on react-native-nitro-modules 0.37.1 (a new autolinked native module), the Expo SDK 57 patch releases (13a0fa5d) and the transitive security refresh. The OTA guard now proves native compatibility with the Android native fingerprint instead of the manifest heuristic. - Classification: preview-build.
- Source commit:
13a0fa5d(the build'sgitCommitHash, recorded aslastCompatibleSourceCommit); baseline advance2e9659c8; guardcc529921,86c20202and35d0bce0. - Source sync: not recorded.
- Source sync proof: not applicable; no OTA ran for the build itself.
- Target: preview build.
- App version:
0.11.0. - Runtime version:
0.11.0. - EAS artifact: Android preview build
e4583905-ddec-45bb-bcc4-08620f8fb47b; native fingerprint2f488e4d01fca1cdabab675b2f9739802e6824f4(the hash EAS recorded for the build); iOS build: none. - Backend artifact: none.
- Verification: recorded in the baseline's
evidencefield:yarn checkgreen on the13a0fa5dtree,check:expo-sdk-tableOK (15 manifests),expo install --checkup to date, and the local fingerprint at13a0fa5dequal to the recorded hash. - Smoke evidence: emulator boot smoke on Medium_Phone_API_36.1 (in-place upgrade over 0.10.0): the NitroMmkv and NitroModules libraries load, a cold boot is still signed in with the stored session, club cards with photos and live counts render, 홈 and 기록 render data, no crash-buffer entries. Owner install smoke: device List 47, still open on 2026-10-06.
- Monitoring checked: not applicable.
- Rollback pointer: the 0.10.0 binary (build
2a544fb5) keeps OTA88456c6d; OTAs for runtime 0.11.0 reach onlye4583905. - Known gaps: the owner's install smoke.
- Source detail:
apps/mobile/ota-compatibility-baseline.json(evidence), Device checklists (List 47), rebuild log (Dependency upgrade Phase C).
2026-09-29 - Preview Builds 0.9.0 a55c851f And 0.10.0 2a544fb5
- Change: dependency upgrade Phase A (Expo SDK 55 latest, react-native 0.83.10, react-native-keyboard-controller 1.22.5; runtime
0.8.0to0.9.0) and Phase B (Expo SDK 57, react-native 0.86.3, React 19.2.3, react-native-reanimated 4.5.1; runtime0.9.0to0.10.0), each as a new native build. - Classification: preview-build, twice.
- Source commit:
3fee6420(0.9.0) and3caa964f(0.10.0); baseline advances45aba036andd6989763. - Source sync: not recorded.
- Source sync proof: not applicable; no OTA ran for the builds themselves.
- Target: preview build.
- App version:
0.9.0, then0.10.0. - Runtime version:
0.9.0, then0.10.0. - EAS artifact: Android preview builds
a55c851f-5a95-41bb-92f2-ccbc2cc2e5f1(0.9.0) and2a544fb5-85e6-4e70-8462-a615cb66bad0(0.10.0). - Backend artifact: none.
- Verification: the baseline's
evidencefield recordsnpx expo export --platform androidclean (a 15MB Hermes bundle for 0.9.0, 14MB for 0.10.0) and expo-doctor clean except the pre-existing monorepo Metro override, for both. - Smoke evidence: emulator boot smokes on Medium_Phone_API_36.1 for both builds (cold boot signed in, club cards render, session push and pop, no crash-buffer entries; the 0.10.0 run also showed the unsaved-changes prompt through Expo Router's forked navigation context). Owner install smokes: device Lists 43 and 44, pending when recorded.
- Monitoring checked: not applicable.
- Rollback pointer: OTAs for a runtime reach only its own build; the 0.8.0 binary keeps OTA
dbcb379e. - Known gaps: the 0.11.0 build (entry above) superseded both as the baseline on 2026-09-30.
- Source detail:
apps/mobile/ota-compatibility-baseline.json(evidence), Device checklists (Lists 43 and 44), rebuild log (Dependency upgrade Phases A and B).
2026-09-05 - Naver-Priority Venue Validity Chain Deployed
- Change: the prepared
00585-00598chain (venue classification/metadata corrections, observation quarantine, source-precedence and attachment guards, court-count contract, successor write guard, Naver identity scope repair, plus the interleaved00590/00591function decompositions) applied to the linked project; guardedenrich-venues,ingest-venues,corroborate-venuesandseed-scenarioredeployed; preview OTA published. - Classification: backend deploy + preview OTA. Owner instruction: "the other agent finished working. Work on the rest."
- Source commit:
f88709b6(main; upstream-synced at push time). - Order followed (validity-review runbook): remote head confirmed at
00584; recovery point taken (.audit/pre-push-2026-09-05/{schema,data}.sql+ JSON corpus, 3,492 open venues) — no scheduled writer inside the window (corroborate-venuescron is 18:15 UTC; push ran 07:40-08:05 UTC); the three guarded functions deployed first; thensupabase db push. - Incident during application:
00590's equivalence-proof fixture calledgen_salt('bf')unqualified and failed on prod (db pushhas noextensionson the search path);00585-00589had applied, the failing file rolled back, nothing recorded past00589. Fixed by schema-qualifyingextensions.crypt/extensions.gen_salt(commitf88709b6), re-validated with a from-scratchsupabase db reset --localthrough00598, then the push resumed and finished. Remote head is00598;migration listshows nothing pending. - Hosted smoke (read-only, dev account via data API):
count_venues_by_viewall=3,292 court=2,407 practice=885 shop=191 (156 ms);search_venues_ranked5 rows (895 ms); table reads OK; 121 closed venues (8 newly closed by00589consolidation; 3,484 open). - Checker (
yarn qa:dataon the recaptured corpus): C1 region-missing-adm2 29→0, C5 kind-unknown 4→0, C5 full-court-on-building-floor 22→12, C7 duplicates 8→6→2, C4 court-index names 7→4. One count rose, C4 name-without-tennis-token 51→53:어시스트링(pre-push drift) and신세계스포렉스(renamed to its Naver canonical name by00598) — both are the source rule working as designed; baseline re-locked at the new counts. - OTA: preview group
eca6a875-ab42-4dd1-a46d-ff56948b6ddd, runtime 0.8.0, sourcef88709b6; native guard passed (no native-sensitive change since6d5c3f05). Carries the god-file refactor waves A-C + venue-finder split and the court-count ceiling (40) entity rule. - Rollback pointer: source revert is not a database rollback; the recovery point above is the restore path (discarding intervening writes). Venue writers were never paused explicitly — none was scheduled in the window.
- Known gaps: the 2,347-row legacy Naver Place exact-ID refetch cohort is still owed; device QA of the refactor OTA is owed (owner).
2026-09-05 - Naver-Priority Venue Validity Closeout Prepared
- Change: audit the 3,492-open-venue capture under the Naver-leading source rule; prepare reviewed classification and metadata corrections, reversible observation quarantine, provider-attachment and source-precedence guards, bounded court-count handling, and matching client/editor validation. The prepared chain is migrations
00585-00596, including the already-committed interleaved00590and00591migrations. - Classification: backend-deploy plus OTA-safe client source prepared; no artifact attempted.
- Source commit:
4dc0ec703205ddeacd8bac851a42db955091615a. - Source sync: upstream-synced.
- Source sync proof:
git rev-parse HEADandgit rev-parse @{u}both returned4dc0ec703205ddeacd8bac851a42db955091615a;git log --oneline @{u}..HEADwas empty after push. The OTA source guard was not run because no OTA/build artifact was published. - Target: no artifact.
- App version:
0.8.0unchanged. - Runtime version:
0.8.0unchanged (runtimeVersion.policy = appVersion). - EAS artifact: none; no build or OTA command ran.
- Backend artifact: none. A read-only linked migration check ended at
00584; migrations00585-00596remain unapplied, and none of the changedenrich-venues,ingest-venues, orcorroborate-venuesfunctions was deployed. - Verification:
yarn checkpassed all 5,154 tests plus docs, architecture, size, and typecheck gates;yarn db:testpassed 1,251 pgTAP assertions across 81 files; the seven direct script suites passed;yarn lint:strict,yarn lint:pi-schema,yarn docs:check,yarn check:sizes,yarn lint:agents-md,yarn verify:no-stale-claims, and staged diff checks passed. A disposable Supabase shadow database applied the complete migration chain through00596; the subsequent schema-diff rendering stopped on the installed CLI's unsupported UDF-dependent check-constraint comparison, after migration application had completed. - Smoke evidence: no hosted smoke was run because nothing was deployed. The deployment runbook requires preflight against a fresh hosted-data snapshot, an ordered maintenance window, and a read-only hosted venue smoke before writers or client distribution resume.
- Monitoring checked: not applicable; no hosted or client artifact exists for this closeout.
- Rollback pointer: before deployment, revert the source commit. After database application, use an owner-reviewed forward migration or the verified recovery point; a Git revert is not a database rollback. Migration
00593can restore only one exact quarantined observation when the current provider slot is empty and attachment validation passes. - Known gaps: 2,347 named legacy Naver Place observations remain an exact-ID refetch cohort if the guarded chain applies; postdeployment capture and checker results are required for final hosted counts. Migrations, edge functions, hosted smoke, build, and OTA remain deliberately deferred.
- Source detail: Venue validity review, venue data authority, and rebuild log.
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.
What only a device can show
Gathered 2026-11-13 from the readiness and UI/UX lane reports. Each line is a flow to try, never a result. The numbered script is List 53; the item is in brackets.
Start and system
- Cold start in light and dark: the splash is white or near-black with no flash; the system bars, keyboard and date picker follow 화면 모드 and the phone [A2, A5].
- A forced font failure and the root crash screen cannot be staged by hand; only a debug throw would show them. Not scripted.
- Android merged manifest: no Camera permission; backups off; the notification colour on the light and dark shade [C7, I5].
- Photo-library and location sentences (iPhone prompts; the Android picker) [C7].
- Names clipped at real widths and 200% text; layout of the club header; FlashList recycling blanks; keyboard and insets (jest checks only props) [A6].
- Swipe-to-delete on a notification row and a feed row: jest proves only the accessibility action [I1].
- Pull to refresh, the FAB position above the tab bar and its collapse, the filter Sheet's scroll, the region picker over the filter Sheet [A4, C4].
- The unsaved-input guard on a scrim press and Android back for the six guarded sheets [D1].
- A tab press scrolls to the top (the library behaviour is read, not seen) [A4].
Network and sign-in
- Offline strip timing and the held write controls; a queued write replays once [L1, L2].
- The 20 s read deadline on a slow link; the 120 s upload; the web probe behind a captive portal (browser) [L1].
- A request refused because the session is gone: never empty lists shown as truth [L4].
- Sign-out offline and the previous account's push (S34) [I4]; a push arrives and opens the thread [I2].
- Resume after an hour in the background [A8].
Money and clubs
- The 복사 button on the account number and in the 입금 안내 Sheet; the long-press hint no longer shows [E1, E5].
- The refund Dialog's sentence at the 24-hour edge (the phone's clock decides what the Dialog says, the server decides the refund) [E2, E4].
- Ledger download: the share sheet, the file name, Korean text in a spreadsheet app [E5].
- The membership offer on two phones: the first row in 내 클럽, the two-button band at 200% text, the Dialog over the band [C1 to C3].
- The challenge loop, the excusal ask and answer and undo, with the cron jobs' first hosted runs [C9, F4].
Browser and links
- A shared session link and the signed-out invite page in a phone browser and KakaoTalk [K1, K2]; the web photo re-encode and a HEIC pick on Safari [K4]; a desktop browser without the Web Share API: copy, the toast, the Dialog (web).
- The rendered check's remaining questions: skeleton width at 1200 and up; the root link card's type fit; whether the browser chrome follows the in-app dark mode.
Colour and type (batch 59; the list is S44)
- Every changed look of S44 (1) and (2) in light, dark and at 200%; the ink focus ring beside a ball button; the dark platinum tile (the owner's open pair); the web font's weight on a slow link; the web in dark before hydration; the iOS large-content viewer (a gap).
- The fonts on a native build: the four Pretendard weights at the first frame (embedded by the plugin) and the failure path of
useFonts.
Server jobs a hand cannot see
- The watchdog's first verdict, the sink's content and the day-30 scrub: read through
yarn qa:health --hosted --runwith the owner's word.
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.