29 KiB
Claude QA Report — Full-App QA (living report)
Verdict (2026-06-26): R10 FULL ClaudeQAPlan run COMPLETE (A–J + fix phase). 0 open P0–P2; 1 P3 (J-OBS). Found 5 P2 this round (Home dup card, wheel back-stack, duplicate app bar, dark paywall contrast, recovery-phrase wrong store) — ALL fixed + verified live + regression-clean (0 FATAL, content still
enc:v1:). E-GAME-002 confirmed live + pruned. Security cornerstone clean (D1–D7). Fixes in working tree — user commits.This report shows current state only. Fixed issues live here for one confirmation round, then they're pruned to the archived-ID line below (full detail stays in git history). See Report hygiene in
ClaudeQAPlan.md.
Run-state (current)
R10 (2026-06-26) full ClaudeQAPlan run COMPLETE | A ✅ B ✅ C ✅ D ✅ E ✅ F ✅ G ✅ H ✅ I ✅ J ✅ | Fix phase ✅ — 5×P2 fixed+verified live (C-HOME-001 · C-NAV-002 · C-NAV-003 · C-PW-001 · C-SEC-001) | E-GAME-002 confirmed+pruned | 0 open P0–P2; 1 P3 (J-OBS) | Baseline: both FREE, 0 active sessions; build installed both emulators | NEXT (R11 = next session): brief confirmation round → re-verify the 5 P2 fixes hold + regression sweep → prune the 5 Fixed rows → flawless. Optional: J-OBS (P3) touch targets. App fixes in working tree (user commits).
- Uncommitted (user commits):
functions/src/games/onGameSessionUpdate.ts(DEPLOYED live),app/.../MainActivity.kt+app/.../notifications/PartnerNotificationManager.kt(E-GAME-002). + Foreground game-alert feature (this session, app rebuilt+installed both): NEWnotifications/GamePromptController.kt,ui/components/GamePromptBanner.kt; editedcore/navigation/AppNavigation.kt,core/notifications/AppMessagingService.kt,ui/home/HomeScreen.kt,ui/home/HomeViewModel.kt,ui/wheel/WheelSessionViewModel.kt. Note: reinstalling the debug APK can leave a stale FCM token until re-register on next launch. - Foreground "partner started a game" alert + bold Game Waiting card (R10, user-requested) — DONE+VERIFIED. When the app is OPEN and a partner starts a game, a prominent in-app top banner (" started " + Join) slides in (mirrors chat's in-app surface) instead of the easy-to-miss system notification; Join → joins the game. Verified live for all 4 session games: banner name correct (Spin the Wheel / This or That / How Well Do You Know Me / Desire Sync); Join → joins wheel (ToT shows graceful "QA is playing a Wheel game" when types mismatch); suppressed when already on that game's screen (added
ActiveGameSessionMonitor.enter/leavetoWheelSessionViewModel— the others already had it). Home "Game waiting" card redesigned as a bold purple-gradient hero (glyph + game name + "Join the game"), promoted to top of "Waiting for you", verified both themes → tap joins the specific game (not the Play-hub fallback). FCM transport on the emulators is flaky (FcmRetry); the banner was exercised via a data-only high-priority send to the partner token (faithful to the deployed payload). - Pass C progress (R10): Settings family ✅ (dark + Security on light): Settings list, Subscription, Security, Delete account, Notifications all render clean both-relevant-themes; 4 illustrations confirmed in-context (Security padlock, Delete-account doorway, Quiet-hours moon, + Subscription); back-stack OK (Security/Delete/Notif → BACK → Settings). Found C-SEC-001 (P2) — accepter's Recovery phrase disabled + wrong "invite your partner" copy (see Open issues). Wheel back-stack RE-CHECKED = not a trap: live wheel session → BACK → spin/setup → BACK → Play hub (2 backs, no dead-end); earlier "stuck" was automation cycling. Leaving mid-wheel leaves a resumable abandoned active session (normal; cleaned). Home ✅ both themes (stale game card gone).
- 6. Spin the Wheel ✅ — spun→Physical Intimacy→session; Sam joined at Q1; both answered 10 (A/B + 1–5 scale + free-text); per-Q You/QA breakdown renders; completed, 0 active. (Helper
wheel_drive.pyhandles mixed types; free-text Qs hide "Next" behind IME.) - 7. Date Match ✅ — swipe deck ("Swiping with Sam/QA"); QA+Sam mutual like → "It is a match!" modal live; new match persisted (date_matches 3→4); swipe action
enc:v1:at rest (only swipedAt clear). - Pass B = COMPLETE (R10): all 7 games played end-to-end 2-device, 0 bugs. 2 observations: CC day-counter desync (Future.md, by-design?) · WATCH — wheel back-stack: after finishing Spin-the-Wheel, system-BACK from the results re-enters the completed wheel-session screen (loop), needed an app relaunch to escape. Possibly automation artifact (missed taps) — recheck deliberately in Pass C nav fuzzing; file if reproducible (P2 back-stack).
- 5. Memory Lane ✅ — new capsule sealed (3-mo pick) with future open date; title+content
enc:v1:at rest (admin-verified); lists cross-session. Minor cosmetic: "Opens in 2 mo" shown for a 3-month selection (relative-time display nit; not filed). - 4. Connection Challenges ✅ — Gratitude Week (in-progress from R9): per-day step, "I did it today", "waiting for partner" both-gate, missed-day catch-up ("Pick it back up"), streak 🔥→2 synced both devices. UX note (Future.md): "Day N of 7" counter diverges between partners after asymmetric catch-up (QA D4/Sam D3) while streak stays synced — plausibly by-design, non-blocking.
- Pass B progress (R10): 1. This or That ✅ — Deep×10 (varied): QA started, Sam joined via Play-hub card (no duplicate, 1 session), both answered 10, results symmetric both devices ("8/10 in sync", per-Q Match labels correct), session→completed, 0 stale. 2. How Well ✅ — QA-subject 5·Quick: QA answered 5 about self, Sam joined as guesser (asymmetric join works), predicted 5, score+breakdown render correctly (1/5, ✓/✗ guess→actual incl. scale Q), completed, 0 stale.
- R10 scratchpad drivers (reuse):
r10_set_premium.js <QA|Sam> <on|off>·rv_gate.js/rv_markreveal.js(raw-API) ·hw_drive.py <serial> <rounds>(taps first option+Confirm per Q) ·rv_inspect.js/rv_sessions.js(admin reads). Game-option taps: use uiautomator bounds, NOT fixed coords (layouts shift per question; last Q button = "Done →" not "Confirm →"). - Admin writes: user authorized this session (2026-06-26) → premium toggle + baseline reset now working. Baseline reset done (0 active sessions; stale 06-24/06-25 answers cleared). Premium toggle:
scratchpad/r10_set_premium.js <QA|Sam> <on|off>. - Pass A ✅ (R10): neither-premium → Desire Sync shows 🔒 + opens paywall ("Go deeper together"); toggled Sam premium ON → QA(free) Play hub badge cleared live + Desire Sync opens setup (no paywall) = couple-shared unlock holds. Code audit: all gates use
CouplePremiumCheckerexceptSubscriptionScreen(by-design own-status) +DailyQuestionResolver(per-user premium-question fallback — verify in Pass B/E it doesn't desync the couple's daily Q). Other 7 features share the verified path (R9 enumerated each). - Build: HEAD
e6a8dee— clean working tree (reveal feature committed: couple-key encryption, read-gatedsecuresubdoc,onAnswerWrittenboth-answered copy,onAnswerRevealed). Rebuilt + installed on both emulators this session. - Daily-reveal QA (2026-06-26, live, both emulators 5554 dark / 5556 light): Gate (raw API): only-1-answered → partner reads metadata 200 but content 403, non-member 403/403; both-answered → partners read each other 200/200, non-member still 403/403. At-rest: answer doc content-free metadata only; content in gated
secure/payload(enc:v1:). Reveal: shows the partner's answer both directions (the fixed bug) — QA↔Sam. Pushes:onAnswerWrittenfires (both-answered "unlocked ✨" copy is in deployed code);onAnswerRevealedfired live (isRevealedflip → "notified partner that X opened"). 0 FATAL either device. Today's test answers wiped after; baseline clean. One low-sev robustness note →Future.md(revealisRevealedwrite isn't retried if it fails). Note: stale active wheel session + 06-24/06-25 unrevealed answers are pre-existing test pollution (confound the Home dashboard daily card; not the reveal feature). - Devices / accounts: emulator-5554 = QA (
Y05AKO2IlTPMa0JQW1BiNIM0uzK2) · emulator-5556 = Sam (imDjjO…) · paired, coupleIdXal3Kw3gjSdn0niERYKJ, both free (baseline restored). - Docs: Playbook
ClaudeQAPlan.md· CoverageClaudeQACoverage.md· IdeasFuture.md## QA· BrandingClaudeBrandingReview.md.
Severity board
| Severity | Open | Fixed (pending 1 confirm) |
|---|---|---|
| P0 | 0 | 0 |
| P1 | 0 | 0 |
| P2 | 1 | 5 |
| P3 | 2 | 0 |
Issues — R10 (1 open P2 [C-DARKART-001] · 5×P2 fixed pending 1 confirm · 2 open P3 [J-OBS, C-ART-EDGE-001])
Each P2 below was found in R10's report-only passes, then fixed + verified live in the R10 fix phase (build succeeded, both emulators reinstalled, 0 FATAL, content still
enc:v1:at rest). Per hygiene they survive one confirmation round, then prune. Fix commits are in the working tree (user commits). Quick fix summary: C-HOME-001buildPendingActions().filterNot{it.target==primary?.target}· C-NAV-002popUpTo(WHEEL_SESSION){inclusive}on session→complete · C-NAV-003 removed WHEEL_HISTORY/GAME_HISTORY/PARTNER_HOME fromshellBackRoutes· C-PW-001BenefitPilltext→PurpleDeep· C-SEC-001SecurityViewModelreadsencryptionManager.recoveryPhrase(coupleId)(CoupleKeyStore) + corrected copy.
| ID | Sev | Area | Description | Repro | Suggested fix | Status |
|---|---|---|---|---|---|---|
| C-ART-EDGE-001 | P3 | Art / edge treatment (Pass C+H, R10) | Displayed illustrations have hard edges instead of fading into the screen. BrandIllustration (BrandIllustration.kt:35-39) hard-clips art to RoundedCornerShape(28.dp) + a hairline border, and EmptyState (EmptyState.kt:43) renders raw painterResource — so the art's near-white/tile background reads as a crisp rounded-rectangle card boundary on the screen (especially visible on dark theme) instead of blending in. Affects every tiled illustration app-wide, both themes. |
Any art screen (e.g. Security padlock, Memory Lane, empty states): the illustration shows a hard tile edge/outline rather than feathering into the background. | Feather the edges to transparent in the shared BrandIllustration/EmptyState helpers (radial/linear fade mask via graphicsLayer{compositingStrategy=Offscreen} + drawWithContent BlendMode.DstIn gradient), or a vignette matching the surface; OR ship transparent/feathered-edge art. Apply once in the shared helpers for consistency. |
Open (P3) |
| C-DARKART-001 | P2 | Theme / dark-mode art (Pass C, R10) | Dark-mode illustrations don't follow the IN-APP theme switch — only the system dark mode. The in-app toggle (Settings → Appearance → Dark) swaps Compose colors via CloserTheme(darkTheme=…) but there's no AppCompatDelegate.setDefaultNightMode/config uiMode override, so painterResource + the drawable-night-nodpi/ variants resolve off the system uiMode. Result: a user who switches the app to Dark while their phone is in light mode gets dark UI + light illustrations (bright tile clashing on the dark screen). Affects all 12 -night illustrations. |
5554: cmd uimode night no (system light) → Settings → Appearance → Dark → Security shows the light padlock tile on a dark screen. With cmd uimode night yes the dark aubergine variant correctly appears → proves the art follows system, not the app. |
Drive the resource uiMode from the in-app theme: a themed Resources in the shared BrandIllustration helper (load the bitmap from a createConfigurationContext with UI_MODE_NIGHT_* set from the app theme), OR AppCompatDelegate.setDefaultNightMode / applyOverrideConfiguration. Verify every -night screen after. |
Open (P2) |
| C-NAV-003 | P2 | Nav / duplicate header (Pass C, R10) | Two stacked app bars (double back arrow) on Wheel History / Past Games — regression of the C-CC-001 class. WHEEL_HISTORY + GAME_HISTORY are in shellBackRoutes (AppNavigation.kt:615-616) so the shell draws a "Wheel History" back bar, but GameHistoryScreen (WheelHistoryScreen.kt:69-72) renders its own Scaffold+TopAppBar("Past Games") → two title bars + two back arrows stacked. A grep of every shellBackRoutes screen shows exactly two that own a TopAppBar: GameHistory (verified live) and PartnerHome (PartnerHomeScreen.kt:228, PARTNER_HOME in shellBackRoutes:594, reachable via Home StreakCard onPartner) — same defect, code-confirmed. |
5554: Play → Spin the Wheel → History → screen shows "← Wheel History" bar over "← Past Games" bar over "Past games" content. | Remove WHEEL_HISTORY, GAME_HISTORY, PARTNER_HOME from shellBackRoutes (the screens own their headers) — exactly the fix applied to CONNECTION_CHALLENGES for C-CC-001 (see comment at AppNavigation.kt:609). |
Fixed + verified live R10 (working tree; user commits) |
| C-PW-001 | P2 | Paywall / dark-mode contrast (Pass C, R10) | Paywall "What's included" benefit pills are near-invisible in dark theme. BenefitPill (PaywallScreen.kt:246) draws a hardcoded light background CloserPalette.PurpleMist but sets text color = MaterialTheme.colorScheme.onSurface, which is near-white on dark → light-on-light, text barely legible (the PurpleDeep checkmark stays visible). Light theme is fine. Same class as the fixed C-DS-001. |
5554 (dark): Play → Desire Sync (both free) → Paywall → "What's included" list rows show white pills with unreadable labels (confirmed via crop; text present in hierarchy). 5556 (light): same rows legible. | Give BenefitPill text a fixed dark brand color (e.g. CloserPalette.PurpleDeep / 0xFF56306F, matching the checkmark) since the pill background is always light, OR make the pill background theme-adaptive. |
Fixed + verified live R10 (working tree; user commits) |
| C-NAV-002 | P2 | Nav / back-stack (Pass C, R10) | Finishing Spin-the-Wheel → results → system BACK re-enters the completed Wheel Session play screen (shows "10/10, Finish/Skip/End session"), then BACK again → Wheel hub. The session→complete nav uses plain navigateRoute (else → navController.navigate, no popUpTo), so the finished play screen stays on the back stack. This is the "WATCH — wheel back-stack" item flagged in B; now deliberately reproduced on 5556 (not an automation artifact). |
5556: play a wheel to completion → auto-lands on Complete/results → BACK → lands inside the finished Wheel Session screen. | When navigating WHEEL_SESSION→WHEEL_COMPLETE, popUpTo(WHEEL_SESSION){inclusive=true} (or pop in WheelSessionScreen on navigateTo) so BACK from results returns to the wheel hub/Play, not the finished play screen. |
Fixed + verified live R10 (working tree; user commits) |
| C-HOME-001 | P2 | Home / UI redundancy (Pass C, R10) | Home shows the top pending action twice. HomePriorityEngine picks a primaryAction (rendered as the big PrimaryHomeActionCard hero) while buildPendingActions() independently re-adds the same item to the "Waiting for you" list — no exclusion of the primary. Seen live: "Challenge waiting" appears both as a compact "Waiting for you" row AND as the prominent hero below it. Applies to every pending type (reveal/partner-answered/game/challenge/date/capsule) whenever the engine's primary overlaps the pending list. |
5554 Home (paired, challenge in progress): two "Challenge waiting" cards stacked (list row + hero). | In buildPendingActions() (or at the call site) drop the card whose target matches primaryAction.target so each waiting item surfaces once. |
Fixed + verified live R10 (working tree; user commits) |
| C-SEC-001 | P2 | Security / recovery — wrong store (Pass C+D4, R10) | SecurityScreen reads the wrong recovery-phrase store, so the accepter can't view/copy their phrase there. Two stores exist: RecoveryPhraseStore (global closer_recovery/recovery_phrase) is written only by the inviter's InviteRepositoryImpl.createInvite (line 43); the accepter's CoupleEncryptionManager.unwrapAndStore persists the phrase in CoupleKeyStore (recovery_phrase_$coupleId, line 71). SecurityScreen (SecurityScreen.kt:82,92) reads recoveryPhraseStore.load() (global) → null for the accepter → row greyed + misleading copy "becomes available after you invite your partner". EditProfileViewModel (reads encryptionManager.recoveryPhrase(coupleId) → CoupleKeyStore) shows it correctly for both. D4 verified the accepter DOES hold the phrase and CAN recover the couple key — E2EE recovery is sound (not data-loss); this is a UI wrong-source bug. |
5554 (QA, accepter): Settings → Security → Recovery phrase greyed + that copy. 5556 (Sam, inviter): active. EditProfile shows it for QA. | Make SecurityScreenViewModel read the phrase via encryptionManager.recoveryPhrase(coupleId) (CoupleKeyStore, the same source EditProfile uses) instead of the global RecoveryPhraseStore; fix the unavailable-state copy. |
Fixed + verified live R10 (working tree; user commits) |
| J-OBS | P3 | A11y / touch targets | A few conversation icon-buttons measure ~42–45dp wide (48dp tall) — single-axis marginal miss of the 48dp target; fully operable. Most controls are 48dp. | Pass J: uiautomator bounds on conversation → 2–3 clickables <126px wide. |
Bump those icon-buttons to 48dp min (e.g. Modifier.minimumInteractiveComponentSize() / size(48.dp)). |
Open (P3, non-blocking) |
Resolved & confirmed (archived — full detail in git history)
A-001 · A-003 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DS-001 · C-NAV-001 · D-001 · E-001 · E-002 · E-003 · E-GAME-002 · E-OBS · F-OBS · F-RACE-001 · I-001 · I-002 — all fixed and re-verified (E-GAME-002 confirmed live R10: startNotifiedAt set + partner_started_game queued to right partner + foreground banner + Join→joined active ToT at same Q1; commits 6e79cd9/38fdc6d) (commits in history; F-RACE-001 re-confirmed R8; I-001 query→whereIn(dayKeys) + I-002 Long-score→Number.toInt(), fixed ab29f6b, re-confirmed live R9: 0 outcomes denials/CCE). Pruned per the one-confirmation-round rule. (C-OBS / outcomes list / SubscriptionScreen per-user gate = investigated, not bugs.)
Security cornerstone — clean (Pass D, deep dive, Round 7)
- D1 at-rest: chat text +
lastMessagePreview+ all 4 game-answer collections (ToT / How Well / Desire Sync / Wheel, both users) + Memory Lane capsules + date-swipe actions =enc:v1:. No plaintext content; only metadata in clear. - D2/D3 access: non-member denied all reads/writes (raw Firestore REST → 403); real premium write
users/{uid}/entitlements/premiumdenied (server-only → no self-grant); cross-couple denied. - D4 keys: couple key phrase-wrapped (argon2id); recovery phrase server-blind;
encryptedRecoveryPhrasewiped on acceptance; plaintextinviteCodenot exploitable (invite readable only by inviter; no code-encrypted secret persisted). - Robustness: malformed/abusive deep-link intents (unknown type, missing extras, injection/path-traversal) → 0 crash; killed-state cold-start chat deep-link → conversation loads.
⛔ "All notifications broken / app opens-and-closes" — ROOT CAUSE = splash crash (FIXED R10)
The actual cause was NOT routing — it was a crash in the splash-screen exit animation on notification cold-starts.
MainActivity.onCreate (added in 95cad84, 2026-06-25) set splashScreen.setOnExitAnimationListener { provider -> provider.iconView.animate()… }.
On a notification / PendingIntent cold-start the OS hands the splash view over without an icon (SplashScreenView: Icon: view: null),
and provider.iconView throws an internal NullPointerException (SplashScreenViewProvider$ViewImpl31.getIconView) →
onCreate crashes → "Force finishing activity" → the app opened and immediately closed on EVERY notification tap
(chat, game-start, results — all of them, because they share the cold-start path). This is why it looked like "all
notifications broke again." Normal launcher cold-starts were fine (icon present), which masked it.
- Why my earlier
am starttests missed it: shellam startuses a different splash transfer than the FCM PendingIntent handover (the SysUILaunch remote transition), so it didn't hit the null-icon handover. Alsoam force-stopcan't receive FCM at all (stopped-package broadcast exclusion) — must useam killto test killed-app push. - Fix (R10, working tree):
MainActivitywraps the icon scale inrunCatching(best-effort) and the view fade inrunCatching { … }.onFailure { provider.remove() }so the splash is always removed and onCreate never crashes. - Verified live: real FCM notification → killed (
am kill) Closer2 → tapped the OS notification → cold-start logsIcon: view: nullthenremove starting view, 0 FATAL, process stays alive, lands on Home (was the crash). Normal launcher cold-start still animates + works.
Notification deep-link routing — SINGLE mechanism (do NOT reintroduce a second one)
Invariant: an app-posted notification carries the resolved route in one place — the app_route extra —
and routing is MainActivity.deepLinkRouteFromIntent → pendingDeepLink → AppNavigation navigateRoute. Do not
also set an ACTION_VIEW + closer:// data Uri on the notification intent: for routes that have a navDeepLink
(conversation / answer_reveal / daily_question / question_thread / home / play) the NavController auto-handles that Uri
in addition to pendingDeepLink → a race/duplicate nav. That dual path is what kept re-breaking notifications.
- Why it broke "again" (root cause, traced via git):
aaab768/1b9d8cf/b9b1560built routing on thecloser://data Uri (NavController auto-handle) + apendingDeepLinkgated oncurrentRoute == HOME; then38fdc6dadded theapp_routeextra on top without removing the data Uri → two mechanisms for the same tap. The HOME-only gate also meant a warm tap from any non-Home tab setpendingDeepLinkbut never consumed it. - Fix (R10, working tree):
PartnerNotificationManager.showNotificationno longer setsACTION_VIEW/data Uri —app_routeextra only.AppNavigationpendingDeepLink gate broadened from== HOMEto!in entryRoutes(fires once past onboarding, on any main screen). Verified live (0 FATAL): killed-app tap → chat opens the conversation; all 4 game results pushes (partner_finished_game) load the real per-session results (wheel "Here's how you each answered" · This-or-That "5/5 in sync" · How Well "Perfect read 5/5" · Desire Sync "5 shared desires"); app_route-only path (no Uri) loads results; warm tap from Settings now routes (was the stuck case).
Round history (one line each)
- E-GAME-003 (2026-06-27) — FIXED+VERIFIED+DEPLOYED: async-game first-finisher left the waiting partner un-notified.
Async games (this_or_that/wheel/how_well/desire_sync) write answers to
couples/{c}/{gameType}/{sessionId}and the session only flips tocompletedwhen BOTH answer — soonGameSessionUpdate(watches the session doc) never fired on a single finish, and the waiting partner got nothing ("Closer2 finished a game but the partner was never notified"). Fix = new Cloud FunctiononGamePartFinished(trigger on the answer doc; on exactly-1 answer, idempotently claimpartFinishNotifiedAton the session + sendpartner_completed_part"X finished their part — your turn to play!"). Verified live: QA finished ToT part → sessionpartFinishNotifiedAt=true, Sam queue got 1partner_completed_part, posted on Sam's device, tap → opened ToT, 0 FATAL. Deployed (onGamePartFinishedcreated,onGameSessionUpdateupdated). Funcs source uncommitted (user commits). - R10 (2026-06-26) — FULL ClaudeQAPlan run A–J + fix phase. Found 5 P2 in report-only passes, fixed + verified all live: C-HOME-001 (Home dup pending card), C-NAV-002 (wheel results→BACK re-entered finished session), C-NAV-003 (duplicate app bar on Wheel History/PartnerHome), C-PW-001 (dark paywall pills light-on-light), C-SEC-001 (Security read wrong recovery-phrase store → accepter couldn't view phrase; E2EE recovery itself sound). E-GAME-002 confirmed live (startNotifiedAt set + partner_started_game→right partner + foreground banner + Join→joined active ToT) → pruned. D1–D7 security clean (non-member denied all raw-API reads/writes, no self-grant, secure-subdoc gate correct, argon2id+AAD=coupleId). Concurrency double-start→1 session. Perf jank 5.53% / a11y font-2.0 reflows — no regression. Build OK, both emulators reinstalled, 0 FATAL, content still
enc:v1:. App fixes in working tree (user commits). - Notif→game fix + dark art + QA sweep (2026-06-26, uncommitted). E-GAME-001 (P1, FIXED+VERIFIED): game notifications "led nowhere" — backgrounded/warm taps landed on Home (MainActivity was standard launch mode →
onNewIntentnever delivered the tap's extras →pendingDeepLinkunset), and even when routed, the game screen showed setup instead of joining (one-shotgetActiveSessionForCoupleraced the post-push Firestore sync → returned stale-empty). Fixes:AndroidManifestMainActivity launchMode=singleTop+QuestionSessionRepositoryImpl.getActiveSessionForCouplenow SERVER-first (cache fallback). Verified live: Sam backgrounded → taps partner_started_game → lands IN the active This-or-That (1/10), joined, no duplicate session; back-stack sane (game→back→Home→back→exit, C-NAV-001 holds). Generic across game types (shared routing + getActiveSession). Dark-theme art: 12_darkvariants →drawable-night-nodpi/(light names) so dark mode auto-swaps; verified live (Security shows the aubergine variant on dark; light unchanged). QA sweep: tabs both themes, deep-link back-stack, all 12 illustrations both themes — 0 FATAL, baseline intact. - Brand art drop (2026-06-26) — wired + QA-swept, 0 issues. All 11 generated illustrations (A1–A12, source gitignored) wired into their screens via shared
EmptyState+ newBrandIllustrationhelper (commits077a408→5868d06). Complete both-theme sweep: in-context dark and light verified for Bucket List (A6), Quiet hours (A9), Security (A11), Delete account (A12) — all render as crisp rounded tiles, on-brand, no clipping/contrast issue; A1 (transparent), A3 (banner) + the empty-only states (A2/A4/A5/A8/A10, unreachable on the baseline couple) verified via the debug Art-preview gallery on both themes + the proven shared tile. 0 FATAL/ANR both devices; baseline intact (0 sessions/outcomes). Process catch: 5556 was on a stale build mid-sweep → reinstalled current, both now on768f511. Details inClaudeBrandingReview.md. - R9 — clean confirmation round (0 new findings): confirmed + pruned I-001/I-002 (0 outcomes denials/CCE on the fixed build); swept deferred Pass C deep/list screens (Answer History, Activity, Bucket List, Date Match/Matches — both themes) + Pass F network (offline cache render + clean reconnect). 0 open P0–P2.
- R8 — F-RACE-001 re-confirmed + pruned; Passes I (perf) + J (a11y) run; found+fixed+verified I-001 & I-002 (outcomes read: query rules-denied + Long/Int parse CCE → "Your Progress" was silently dead). 0 open P0–P2.
- R7 — multi-angle security/concurrency deep dive → cornerstone fully clean; F-RACE-001 found + fixed + verified. 0 new open.
- R6 — branding drop + Future.md backlog regression (white-keyhole icons/loader/splash, inclusive gender, copy, rate-limit split, results-push suppression, paywall retry/offline) → 0 new open.
- R5 — Cloud Functions deployed (E-OBS channel fix, E-003 results routing) + new Pass G (account creation / fake-account abuse) clean → 0 open.
- R1–R4 — baseline Passes A–F report-only; every P0–P2 found was fixed + verified (see archived IDs).
Operational constants
- Execution mode: autonomous run-to-completion — don't stop; fix blockers inline; cycle fix→re-QA until flawless. Don't hand back when context fills — re-read this run-state + coverage after any compaction. Commit before interruptible work; recover stuck sessions via the session-start ritual.
- Standing authorization (user, 2026-06-24): may
firebase deploy --only firestore:rules+ has admin access (Firestore reads/writes/seeds + entitlement toggles) — run without pausing. Only the macOS requirement for iOS (Parts 2/3) is a hard stop. - Hardening backlog → Future.md: App Check not enforced on Firestore;
users/{uid}update rule allows arbitrary non-hasPremiumfields (tighten to a field allowlist).