Closer/ClaudeReport.md

121 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Claude QA Report — Full-App QA (living report)
> **Verdict (2026-06-26): R10 FULL ClaudeQAPlan run COMPLETE (AJ + fix phase). 0 open P0P2; 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 (D1D7). 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 P0P2; 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):** NEW `notifications/GamePromptController.kt`, `ui/components/GamePromptBanner.kt`; edited `core/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** ("<partner> started <Game>" + 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/leave` to `WheelSessionViewModel` — 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 + 15 scale + free-text); per-Q You/QA breakdown renders; completed, 0 active. (Helper `wheel_drive.py` handles 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 `CouplePremiumChecker` except `SubscriptionScreen` (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-gated `secure` subdoc, `onAnswerWritten` both-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:** `onAnswerWritten` fires (both-answered "unlocked ✨" copy is in deployed code); `onAnswerRevealed` fired live (`isRevealed` flip → "notified partner that X opened"). 0 FATAL either device. Today's test answers wiped after; baseline clean. One low-sev robustness note → `Future.md` (reveal `isRevealed` write 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, coupleId `Xal3Kw3gjSdn0niERYKJ`, both free (baseline restored).
- **Docs:** Playbook `ClaudeQAPlan.md` · Coverage `ClaudeQACoverage.md` · Ideas `Future.md` `## QA` · Branding `ClaudeBrandingReview.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-001 `buildPendingActions().filterNot{it.target==primary?.target}` · C-NAV-002 `popUpTo(WHEEL_SESSION){inclusive}` on session→complete ·
> C-NAV-003 removed WHEEL_HISTORY/GAME_HISTORY/PARTNER_HOME from `shellBackRoutes` · C-PW-001 `BenefitPill` text→`PurpleDeep` ·
> C-SEC-001 `SecurityViewModel` reads `encryptionManager.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-`clip`s 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 **~4245dp wide** (48dp tall) — single-axis marginal miss of the 48dp target; fully operable. Most controls are 48dp. | Pass J: uiautomator bounds on conversation → 23 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/premium` denied (server-only → **no self-grant**); cross-couple denied.
- **D4 keys:** couple key phrase-wrapped (argon2id); recovery phrase server-blind; `encryptedRecoveryPhrase` wiped on acceptance; plaintext `inviteCode` not 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 start` tests missed it:** shell `am start` uses a different splash transfer than the FCM
PendingIntent handover (the SysUILaunch remote transition), so it didn't hit the null-icon handover. Also `am
force-stop` can't receive FCM at all (stopped-package broadcast exclusion) — must use `am kill` to test killed-app push.
- **Fix (R10, working tree):** `MainActivity` wraps the icon scale in `runCatching` (best-effort) and the view fade in
`runCatching { … }.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 logs
`Icon: view: null` then `remove 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`/`b9b1560` built routing on the
`closer://` **data Uri** (NavController auto-handle) + a `pendingDeepLink` gated on **`currentRoute == HOME`**;
then `38fdc6d` added the `app_route` extra **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 set `pendingDeepLink` but never consumed it.
- **Fix (R10, working tree):** `PartnerNotificationManager.showNotification` no longer sets `ACTION_VIEW`/data Uri —
`app_route` extra only. `AppNavigation` pendingDeepLink gate broadened from `== HOME` to `!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 to `completed` when BOTH answer — so `onGameSessionUpdate` (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 Function **`onGamePartFinished`** (trigger on the answer doc; on exactly-1 answer, idempotently claim
`partFinishNotifiedAt` on the session + send `partner_completed_part` "X finished their part — your turn to play!").
Verified live: QA finished ToT part → session `partFinishNotifiedAt=true`, Sam queue got 1 `partner_completed_part`,
posted on Sam's device, tap → opened ToT, 0 FATAL. Deployed (`onGamePartFinished` created, `onGameSessionUpdate`
updated). Funcs source uncommitted (user commits).
- **R10 (2026-06-26) — FULL ClaudeQAPlan run AJ + 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. D1D7 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 → `onNewIntent` never delivered the tap's extras → `pendingDeepLink` unset), and even when routed, the game screen showed *setup* instead of joining (one-shot `getActiveSessionForCouple` raced the post-push Firestore sync → returned stale-empty). Fixes: `AndroidManifest` `MainActivity launchMode=singleTop` + `QuestionSessionRepositoryImpl.getActiveSessionForCouple` now 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 `_dark` variants → `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 (A1A12, source gitignored) wired into their screens via shared `EmptyState` + new `BrandIllustration` helper (commits `077a408`→`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 on `768f511`. Details in `ClaudeBrandingReview.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 P0P2.
- **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 P0P2.
- **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.
- **R1R4** — baseline Passes AF report-only; every P0P2 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-`hasPremium` fields (tighten to a field allowlist).