Closer/ClaudeReport.md

29 KiB
Raw Blame History

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 (" 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/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-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 ~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.

Invariant: an app-posted notification carries the resolved route in one place — the app_route extra — and routing is MainActivity.deepLinkRouteFromIntentpendingDeepLinkAppNavigation 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 077a4085868d06). 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).