Closer/ClaudeReport.md

41 KiB
Raw Blame History

Claude QA Report — Full-App QA (living report)

Verdict (2026-06-27): R13 = fixed the entire open backlog + full fresh AJ — FLAWLESS (0 open P0P3). Took over and fixed all 3 open Codex dark-mode findings (C-DARK-UI-001 P2 This-or-That redesign; C-DARK-UI-002 P3 check-in label/value; C-DARK-UI-003 P3 bottom-inset clipping) plus the 2 carried P3s (C-ART-EDGE-002 direct-call hero feathering; J-OBS 48dp touch targets), and confirmed A-201 (P1) live → pruned. Also shipped the branding Premium-unlock modal (illustration_premium_unlock, one-time, shown to BOTH partners on couple-shared activation). All verified live on both emulators (5554 dark / 5556 light), 0 FATAL. Full fresh AJ run clean: Pass D security cornerstone re-verified LIVE (non-member 403, self-grant 403, member 200, chat at-rest enc:v1:); A premium gates → Paywall (Date Match + Desire Sync); B ToT full both themes + Wheel launch; I jank 6.43% (perf-safe); J 48dp confirmed. Diff is UI-only (no rules/functions/crypto change) → E/F/G carried. All app changes in the working tree — user commits.

Verdict addendum (2026-06-27): ad hoc DARK-MODE UI/brand review on dedicated Codex emulator COMPLETE. Built + installed the current debug APK on my own CloserCodexQA emulator (emulator-5558), forced system dark mode, created a fresh real paired couple through the app invite flow, and swept profile/onboarding, unpaired invite, paired Home, Play, This-or-That, Settings, Notifications, Paywall, Messages, and Today. Button text is generally readable across profile/Home/Settings/Notifications/Messages/Paywall, but the sweep found 1 open P2: This-or-That active gameplay has low-contrast dark option text and an off-brand diagonal/circle backdrop crossing the prompt. Also found 2 open P3s: first-launch check-in modal label/value collision and recurring bottom-inset clipping on scroll content near nav/gesture areas. Logs checked after navigation/game entry: 0 app FATAL/ANR/force-finish; only uiautomator/system noise plus a non-crashing BillingClient unbind warning.

Verdict (2026-06-27): R11 confirmation round COMPLETE — FLAWLESS (0 open P0P2). Fixed the last open P2 (C-DARKART-001 — dark art now follows the in-app theme) + the open P3 (C-ART-EDGE-001 — art feathers into the surface), both in the shared BrandIllustration/EmptyState helpers, verified live on both decoupled theme directions (system-light+app-Dark → dark aubergine art; system-dark+app-Light → light pastel art), 0 FATAL. Re-confirmed all 5 R10 P2 fixes hold (C-HOME-001 single card · C-NAV-002 wheel-back popUpTo present · C-NAV-003 single app bar live · C-PW-001 dark paywall pills legible live · C-SEC-001 recovery row active for accepter live) → pruned. Entrypoint launch-integrity smoke green (splash-crash class clean on the fresh APK). Only remaining: 2 freshly-fixed art items pending 1 confirm + J-OBS (P3 touch targets). Art fixes in working tree — user commits.

📖 Architecture reference: see docs/Engineering_Reference_Manual.md. Most fixed-and-pruned IDs above are documented in its Known landmines and recent fixes section — read before re-touching the affected area.

Verdict (2026-06-26): R10 FULL ClaudeQAPlan run COMPLETE (AJ + fix phase). 0 open P0P2; 1 P3 (J-OBS). Found 5 P2 (Home dup card, wheel back-stack, duplicate app bar, dark paywall contrast, recovery-phrase wrong store) — ALL fixed + verified live + regression-clean. E-GAME-002 confirmed live + pruned. Security cornerstone clean (D1D7). [Pruned in R11.]

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)

  • R13 (2026-06-27) — backlog fix pass + full fresh AJ — FLAWLESS (0 open P0P3). Took over the open Codex dark-mode backlog and shipped it all (working tree, both emulators rebuilt+installed; baseline both FREE, 0 active sessions): C-DARK-UI-001 (ToT dark redesign — theme-aware backdrop/options/chips/versus/progress/pills) · C-DARK-UI-002 (check-in label/value weight) · C-DARK-UI-003 (Play/Home/Paywall bottom clearance) · C-ART-EDGE-002 (8 opaque heroes routed through BrandIllustration feather) · J-OBS (composer/voice/retry buttons → 48dp). Confirmed A-201 live → pruned. Shipped the Premium-unlock modal (ui/components/PremiumUnlockOverlay.kt, hosted in AppNavigation; driven off CouplePremiumChecker, one-time via a new premiumUnlockCelebrated SettingsRepository flag) — verified live on BOTH purchaser (5554) and partner (5556) + one-time gate (dismiss→relaunch no re-show). AJ: A ✓ (Date Match + Desire Sync gates → Paywall) · B ✓ (ToT full both themes; Wheel launch clean) · C ✓ (extensive both-theme sweep) · D ✓ LIVE (non-member 403 ×2, self-grant 403, member 200, chat at-rest enc:v1:) · E/F/G carried (diff is UI-only; no rules/functions/crypto/auth change) · H ✓ (ToT brand + modal + heroes) · I ✓ (jank 6.43%) · J ✓ (48dp). 0 FATAL both devices. Uncommitted (user commits): 16 modified + ui/components/PremiumUnlockOverlay.kt + res/drawable-nodpi/illustration_premium_unlock.png.
  • Ad hoc dark-mode UI/brand sweep (2026-06-27, Codex-owned emulator emulator-5558): current debug APK installed, dark mode forced, fresh real paired users created through invite flow (Codex Dark + River Dark). Swept profile, invite, paired Home, Play, This-or-That, Settings, Notifications, Paywall, Messages, Today. 0 app FATAL/ANR/force-finish in logcat. Findings added below: C-DARK-UI-001 (P2), C-DARK-UI-002 (P3), C-DARK-UI-003 (P3). Screenshots captured at /tmp/closer-dark-04-after-permission.png through /tmp/closer-dark-25-today.png. R12 (2026-06-27) FRESH FULL ClaudeQAPlan run STARTED (user: "start from the start") | Baseline verified clean: QA free, Sam free (premium revoked), 0 active sessions; build HEAD 2cd0af6 + 3 uncommitted art files installed both emulators (5554=Dark, 5556=Light) | A ✅ (A-201 P1 Date-Match premium bypass) | B ✅ (4 async games full 2-device end-to-end + first-finisher nudge + C-NAV-002 + Ready=Start re-verified live; CC/MemoryLane/DateMatch render-checked; MemoryLane title/preview run-on→Future) | C ✅ (regression-clean vs R10 sweep; Messages/Today/Subscription + organic A/B + R11 decoupled art; NEW C-ART-EDGE-002 P3 direct-call hero hard edges on dark) | D ✅ (LIVE: D1 game answers enc:v1: · D3 non-member 403×4 + member-scoped · D5 self-grant→403; D2/D4/D6/D7 carried R7/R10, rules unchanged) — cornerstone clean | E ✅ (smoke 6/6 + Pass B triggers) | F ✅ (concurrency+process-death+offline) | G ✅ (security half live) | H (finding C-ART-EDGE-002) | I ✅ (jank 4.10%) | J (J-OBS P3) | **FIX PHASE ✅ — A-201 (P1) FIXED+VERIFIED LIVE** (Date Match LOVE/MAYBE on premium idea → Paywall via CouplePremiumChecker; SKIP passes; 0 FATAL). C-DARKART-001+C-ART-EDGE-001 held → PRUNED. | **R12 COMPLETE — FLAWLESS: 0 open P0P2.** Remaining: 2 non-blocking P3s (C-ART-EDGE-002 hero edges, J-OBS touch targets). | NEXT (R13): confirm A-201 holds → prune; optional P3 polish. Uncommitted (user commits): R11 art files + ui/dates/DateMatchViewModel.kt+ui/dates/DateMatchScreen.kt (A-201). Carryover from R11 (still valid): C-DARKART-001 (P2) + C-ART-EDGE-001 (P3) fixed pending 1 confirm; J-OBS (P3) open touch targets.
  • **(prior) R11 (2026-06-27) confirmation round COMPLETE — FLAWLESS | Fixed last open P2 C-DARKART-001 (in-app-theme art) + P3 C-ART-EDGE-001 (feathered edges) in shared BrandIllustration/EmptyState; verified live both decoupled theme directions, 0 FATAL | 5 R10 P2 fixes re-confirmed + PRUNED (C-HOME-001/C-NAV-002/C-NAV-003/C-PW-001/C-SEC-001) | entrypoint smoke green on fresh APK | 0 open P0P2 | Baseline: both FREE, 0 active sessions; build (HEAD 2cd0af6 + 3 uncommitted art files) installed both emulators | NEXT (R12 = next session): confirm C-DARKART-001 + C-ART-EDGE-001 hold → prune; optional J-OBS (P3) touch targets; then declare program-complete for Android. Art fixes in working tree (user commits).`
  • Pass B progress (R12): 1. This or That — full end-to-end 2-device, NEW style Light×5 Quick (R10 was Deep×10): QA started → answered 5 (alt A/B) → first-finisher state; first-finisher nudge fired (partFinishNotifiedAt set + Sam queue partner_completed_part "QA finished their part — your turn to play!"); Sam joined via Play-hub active state (at Q1/5, no dup session) → answered all-A → session→completed (0 active); partner_finished_game to BOTH; reveal 3/5 in sync symmetric + correct Match/Differ + You/QA attribution on both devices (QA dark / Sam light). 0 FATAL. 2. Spin the Wheel Ready=Start session (R11 change) verified; spun→Stress→10Q; mixed answer types (free-text + 15 scale) render+accept; Sam joined active session via Play hub (Q1, no dup/new spin); both finished→completed (0 active); results "Here's how you each answered" with You/QA free-text + scale; C-NAV-002 RE-VERIFIED LIVE — results → BACK → wheel hub (Spin/History), NOT the finished session (the R11-deferred confirm). 0 FATAL. 3. How Well — QA subject 5·Quick (answered 5 about self), Sam joined as guesser ("Predict how QA answered…", asymmetric), guessed 5 → score 5/5 "Perfect read" + per-Q breakdown (✓ + answer, choice+scale) symmetric both devices; completed (0 active). 4. Desire Sync (premium, couple-shared via Sam-on) — free QA opened setup (no paywall); QA all-Yes, Sam joined + Y,Y,Y,N,N → reveal "3 shared desires · 2 kept private" (only mutual-Yes shown, 2 mismatches "Private") symmetric both devices; completed (0 active); Sam premium restored OFF. All 4 async session games verified end-to-end.
  • Uncommitted (user commits): R11 art fixes — app/.../ui/theme/Theme.kt, app/.../ui/components/BrandIllustration.kt, app/.../ui/components/EmptyState.kt; + R12 A-201 fix — app/.../ui/dates/DateMatchViewModel.kt (CouplePremiumChecker gate + paywallRequired event) + app/.../ui/dates/DateMatchScreen.kt (navigates to paywall). Everything else committed in 2cd0af6. Build installed both emulators.
  • 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 0 1 (C-DARK-UI-001)
P3 0 4 (J-OBS, C-ART-EDGE-002, C-DARK-UI-002, C-DARK-UI-003)

R13: A-201 (P1) confirmed live → pruned to the archived line. The 5 fixes above are verified-live this round; they live one confirmation round, then prune.

Issues — ad hoc dark-mode UI/brand review (2026-06-27)

Dedicated emulator: CloserCodexQA / emulator-5558; fresh paired users created through the real invite flow; system dark mode forced. This pass focused on brand fit, image integration, dark-mode button/text visibility, route integrity, and logs after navigation/game launch. Clean surfaces: profile creation, invite, paired Home, Settings ("Connected with Codex Dark"), Notifications, Messages, and Paywall primary content/buttons are readable and on-brand in dark mode. Known C-ART-EDGE-002 reproduced: Today's direct-call hero art still appears as a bright pasted light tile on dark, already tracked below.

ID Sev Area Description Evidence / repro Suggested fix Status
C-DARK-UI-001 P2 This or That / dark active gameplay Dark-mode gameplay still looks off-brand and weakly legible. The diagonal line + two-circle backdrop cuts through the question and reads like a forgotten placeholder, especially in dark mode. A/B option labels and option text are too dim on the dark cards; mood-picker duration chips are also low-contrast. This is the one screen where dark button/choice text does not meet the rest of the app's standard. emulator-5558: Play → This or That → choose mood/count → active question. Screenshot: /tmp/closer-dark-20-this-or-that-game.png; setup chip contrast: /tmp/closer-dark-19-this-or-that-mood.png. Logcat after launch: 0 app FATAL/ANR/force-finish. Replace the placeholder-like line/circle motif with branded game art or a subtle surface treatment; use theme tokens with high-contrast option text for dark; verify A/B cards, selected/pressed/disabled states, and all mood/count chips in both themes. Fixed + verified live R13 (working tree)ThisOrThatScreen.kt: ChoicePromptBackdrop redrawn as a soft theme-aware glow + two faint paired-card silhouettes low in the frame (no line/diagram, never crosses the prompt); OptionCard A/B now map to colorScheme.primary/secondary with high-contrast onSurface body text + visible accent borders + rich filled selected state; VersusBadge/progress/pills/mood number-circle/TotLengthChips all theme-aware. Verified both themes (5554 dark / 5556 light): backdrop subtle, options/chips legible, 0 FATAL. Added light+dark previews.
C-DARK-UI-002 P3 Paired Home / first-launch check-in modal Slider label and value collide in dark mode. The second check-in row reads visually like communicate?5 because the label runs into the numeric value. Button text ("Submit", "Skip for now") is readable, but the modal needs spacing/layout polish. emulator-5558: first paired Home launch opened the quick check-in modal. Screenshot: /tmp/closer-dark-16-paired-home.png. Give slider rows a stable two-column layout or wrap/value-align the score independently; verify at default and larger font scales. Fixed R13 (working tree)OutcomeCheckInDialog.kt OutcomeSlider: label given Modifier.weight(1f) and the row switched to spacedBy(12.dp), so the numeric value keeps its own column and can never collide with a long label. Verified-by-construction (the one-time baseline-check-in dialog is gated outcomeBaselineShownAt!=0 on these installs; the change is a deterministic layout fix).
C-DARK-UI-003 P3 Insets / bottom navigation and gesture area Several dark-mode scroll surfaces render useful content too close to or under bottom nav/gesture areas. Play clips the lower "Desire Sync" card behind bottom nav; unpaired Home's bottom CTA is too tight to the nav area; Paywall's "Already subscribed? / Restore" card starts under the gesture area. Text is mostly readable, but it looks unfinished and can hide tap targets. Screenshots: /tmp/closer-dark-18-play.png, /tmp/closer-dark-07-post-profile.png, /tmp/closer-dark-23-paywall.png. Audit scaffold/content padding for bottom nav + system gesture insets; add consistent navigationBarsPadding/bottom spacer to scrollable content and modal/fullscreen paywall surfaces. Fixed + verified live R13 (working tree) — added explicit bottom clearance to the three flagged scroll containers: Play LazyColumn contentPadding = bottom 28.dp; Home column bottom 36.dp; Paywall column bottom 40.dp. Verified live: Play last row (Bucket List / Past Games) clears the bottom nav; Paywall Restore/legal clears the gesture area.

Issues — R12 (0 open P0P2 = FLAWLESS · A-201 P1 fixed+verified-live pending 1 confirm · 2 open P3 [J-OBS, C-ART-EDGE-002])

R12 was a FRESH FULL AJ run. Found A-201 (P1, Date Match premium bypass) — fixed + verified live this round (gated via CouplePremiumChecker). C-DARKART-001 (P2) + C-ART-EDGE-001 (P3), fixed in R11, held through R12's visual sweep and are pruned to the archived-ID line below (detail in git / working tree). New P3 C-ART-EDGE-002 (direct-call hero hard edges) is deferred polish. Remaining open = 2 non-blocking P3s. A-201 fix + R11 art fixes are in the working tree (user commits).

ID Sev Area Description Repro Suggested fix Status
A-201 P1 Premium / Date Match bypass (Pass A, R12) Premium date ideas are NOT gated — free users can view, swipe, like and match them with no paywall. DateIdea.isPremium is documented as "requires an active premium entitlement," but DateMatchRepositoryImpl.getDateIdeas() returns DateIdeaSeed.all (premium ideas included, no entitlement filter), DateMatchViewModel loads them with no CouplePremiumChecker, and DateMatchScreen only renders a cosmetic PremiumBadge() — no lock overlay, no paywall on like/super-like. So the premium tier for Date Match is unenforced. Escaped prior Pass A rounds (which asserted "all gates use CouplePremiumChecker" — Date Match has NO gate). (Plan buckets "premium bypass" as P0; filed P1 as it's a content-tier subset, no security/data impact.) 5554 (QA free): Play → Date Match → reject through deck → reach a ★ Premium idea ("night camping getaway") → Like (heart) → no paywall, swipe accepted, deck advanced to the next premium idea ("Zipline canopy tour"); 0 FATAL, no PERMISSION_DENIED. Gate premium date ideas through CouplePremiumChecker: either filter isPremium ideas out of a free couple's deck, OR intercept like/super-like on a premium idea → route to Paywall when neither partner is premium (couple-shared). Mirror the Desire Sync / Question-pack gate. Confirmed live R13 → PRUNED (added to the archived-ID line). R13 re-verify: free QA → Date Match → Love a ★Premium idea ("Overnight camping getaway") → Paywall, deck did not advance (returned to the same premium card on back); 0 FATAL. Fix is in the working tree (DateMatchViewModel/DateMatchScreen, committed 2cd0af6-era + this confirm).
C-ART-EDGE-002 P3 Art / hard edges on direct-call heroes (Pass C, R12) Hero illustrations rendered via direct painterResource (not the shared BrandIllustration) still show hard edges on dark theme — the R11 C-ART-EDGE-001 feather fix only covered BrandIllustration/EmptyState. The Today "Weekend Side Quest" daily-question hero (light/pink art) renders as a bright rounded-rect block with a hard bottom edge on the dark screen. These direct-call heroes have no -night variant either. Likely same class: paywall couple art, onboarding, Home tonight-prompt/partner-activation, spin-wheel hero, pack art (all direct painterResource(illustration_*)). Matches the user's "all images should fade into the screen" request (only partially satisfied by C-ART-EDGE-001). 5554 (dark): Today tab → daily question → "Weekend Side Quest" hero shows a hard bright edge against the dark card. Route these heroes through BrandIllustration (gains feather + theme-variant), OR apply the same featherEdges() treatment at each call site; consider tile/hero variants. Verify each direct painterResource(R.drawable.illustration_*) site listed in the R12 grep. Fixed + verified live R13 (working tree) — routed the 8 opaque (RGB, no-alpha) hero tiles through BrandIllustration(tile=true): daily_question (the repro), couple_paywall, couple_subscription, couple_onboarding, partner_activation, tonight_partner_prompt, couple_invite, together_empty. Confirmed via identify that spin_wheel / streak_milestone / reveal_celebration are transparent-or-celebration and left as-is (they float). Verified live: Today "Weekend Side Quest" hero now feathers into the dark surface (no hard bright block); paywall hero blends.
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)). Fixed + verified live R13 (working tree)ChatComposer media buttons (40→48dp default), voice play/pause (36→48), recording-cancel (44→48), and the failed-message retry/dismiss (28→48) in ChatComponents.kt + ConversationScreen.kt. Verified live: composer clickables now measure 126px = 48dp on both axes; layout clean (premium-lock badges intact).

Resolved & confirmed (archived — full detail in git history)

A-001 · A-003 · A-201 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DARKART-001 · C-DS-001 · C-ART-EDGE-001 · C-HOME-001 · C-NAV-001 · C-NAV-002 · C-NAV-003 · C-PW-001 · C-SEC-001 · D-001 · E-001 · E-002 · E-003 · E-GAME-002 · E-GAME-003 · E-OBS · F-OBS · F-RACE-001 · I-001 · I-002 — all fixed and re-verified (R13 pruned A-201 [Date-Match premium ideas ungated → now gated to Paywall via CouplePremiumChecker] — fixed R12, confirmed live R13; in working tree) (R12 pruned C-DARKART-001 [in-app-theme -night art] + C-ART-EDGE-001 [feathered edges] — fixed R11, held through R12 visual sweep; in working tree) (R11 pruned the 5 R10 P2 fixes — C-HOME-001 single Home card · C-NAV-002 popUpTo(WHEEL_SESSION){inclusive} present + R10-live · C-NAV-003 single app bar re-confirmed live · C-PW-001 dark paywall pills legible re-confirmed live · C-SEC-001 recovery row active for accepter re-confirmed live — all committed in 9c84c36; E-GAME-003 onGamePartFinished deployed + committed 2cd0af6) (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)

  • R13 (2026-06-27) — open-backlog fix pass + full fresh AJ, FLAWLESS (0 open P0P3). Fixed all 5 carried/open UI items (C-DARK-UI-001 ToT dark redesign · C-DARK-UI-002 check-in label · C-DARK-UI-003 bottom insets · C-ART-EDGE-002 8 opaque heroes feathered · J-OBS 48dp targets), confirmed A-201 live → pruned, and shipped the branding Premium-unlock modal (one-time, both partners, couple-shared). AJ: D security cornerstone re-verified LIVE (non-member 403, self-grant 403, at-rest enc:v1:); premium gates → Paywall; ToT both themes; jank 6.43%. Diff UI-only → E/F/G carried. 0 FATAL both emulators. App changes in working tree (user commits).
  • R12 (2026-06-27) — FRESH FULL AJ run + fix phase, FLAWLESS (0 open P0P2). Found A-201 (P1): Date Match premium ideas ungated (free users could like/match ★Premium ideas — getDateIdeas()=all, no checker, badge only; escaped prior Pass A rounds) → fixed + verified live (gated LOVE/MAYBE via CouplePremiumChecker→Paywall, SKIP passes). Pass B: all 4 async games full 2-device E2E (ToT/Wheel/HowWell/DesireSync) + first-finisher nudge + C-NAV-002
    • Ready=Start re-verified live. Pass D LIVE clean: non-member 403 (read+write), self-grant→403, game answers enc:v1:. Pass E smoke 6/6. Pass I jank 4.10% (art change perf-safe). New P3 C-ART-EDGE-002 (direct-call hero hard edges, deferred). C-DARKART-001+C-ART-EDGE-001 (R11) held → pruned. Retrospective added to Pass A (badge≠gate; try to USE premium content as a free user). Fixes in working tree (user commits).
  • R11 (2026-06-27) — confirmation round, FLAWLESS (0 open P0P2). Fixed the last open P2 C-DARKART-001 (dark-mode art now follows the in-app theme: LocalAppInDarkTheme CompositionLocal in CloserThemeBrandIllustration loads the -night drawable via a createConfigurationContext whose UI_MODE_NIGHT_* comes from the app theme, not the system) and the open P3 C-ART-EDGE-001 (tiled art feathers its 4 edges to transparent via graphicsLayer{Offscreen} + BlendMode.DstIn gradients instead of hard clip+border; EmptyState now routes through BrandIllustration). Verified live both decoupled theme directions (5554 system-light+app-Dark → dark aubergine art; 5556 system-dark+app-Light → light pastel art; both feathered), 0 FATAL, both apps alive. Re-confirmed + pruned the 5 R10 P2 fixes (C-HOME-001 single Home card · C-NAV-002 wheel-back popUpTo present · C-NAV-003 single app bar live · C-PW-001 dark paywall pills legible live · C-SEC-001 recovery row active for accepter live). Entrypoint launch-integrity smoke green on the fresh APK (launcher
    • notification cold-starts open & stay — splash-crash class clean). Art fixes in working tree; everything else committed (2cd0af6).
  • 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).