Closer/ClaudeReport.md

48 KiB
Raw Blame History

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

Verdict (2026-06-27): R15 = gap-closing round on Passes L/M/N/P + smoke — found & FIXED M-001 (P2 quiet hours). Targeted the previously-uncovered gaps (the new "flawless" bar needs L + P clean + M take-effect). Found M-001 (P2): "Quiet hours — 10 PM8 AM, no notifications" did not suppress partner pushes when the recipient app was backgrounded/killed (the main case) — quiet hours was local-only (never synced server-side) and the OS shows the FCM notification block directly without running app code. Fixed + verified live: client now mirrors the window+timezone to users/{uid}; the 4 partner-action senders (onMessageWritten/onAnswerWritten/onAnswerRevealed/onGameSessionUpdate) suppress server-side via a fail-open recipientInQuietHours(); rules allowlist extended for the new fields. Live: QH ON → function logs …is in quiet hours — suppressing, 0 delivery; QH OFF → notified partner, delivery resumes; per-type chat toggle still suppresses (server-enforced). Then drove Pass N (user "FIX"): N-001 (P1) — Bucket List was entirely non-functional (coupleId never wired → all CRUD silently no-op) → FIXED + verified live (add enc:v1:/complete/delete/render). N-002 (P2) — "Plan a Date"/Date Builder "Create Plan" was a no-op (wrote to an unread prefs collection; dateIdeaId/coupleId never wired) → FIXED + verified live (re-pointed to create a PLANNED DatePlan → Home shows "Date coming up"). Outcomes/Your Progress code-correct. Clean passes: L (chat decrypt both dirs, attribution, Seen receipt, ❤️ reaction, ordering, day-sep, inbox no enc: leak, at-rest enc:v1:); P (UI copy warm/inclusive, debug rows BuildConfig.DEBUG-gated, friendly error fallbacks; question bank 6103 Qs — 0 empty, 0 dupes, 0 placeholders, complete answer configs, on-guide tone); daily-Q + reveal gate render; smoke 6/6 GREEN both emulators. 2 P3 brand-asset backlogs still open. 0 FATAL.

Verdict (2026-06-27): R14 = full fresh AJ ClaudeQAPlan run — 0 open P0P2, 0 new functional findings. A pure-QA confirmation round (no code changes) on the R13 build. (A follow-up 2026-06-27 brand-standards audit then opened 2 P3 brand-asset backlogs — every image needs a dark variant; every icon must be custom — see the Issues section + ClaudeBrandingReview.md.) The 5 R13 fixes (C-DARK-UI-001/002/003, C-ART-EDGE-002, J-OBS) held through R14's sweep → pruned; the Premium-unlock modal held + re-verified. Live results: Pass A — premium ENFORCEMENT audited (all 6 gated features have a real gate, not just a badge; A-201 class closed) + free→Paywall confirmed for Date Match / Desire Sync / Question Packs + couple-shared unlock (Sam→QA) with modal + subscription_entitlement_changed push delivered live. Pass B — Desire Sync / How Well / Spin-the-Wheel full 2-device (mixed answer types incl free-text + multi-select + skipped-answer reveal), first-finisher partner_completed_part nudge confirmed live, Memory Lane create+seal (premium), Connection Challenges resume, Date Match deck (ToT carried from R13). Pass C — broad both-theme sweep + decoupled-theme-art mandate (system-light+app-Dark → dark UI + correctly-themed feathered art). Pass D — cornerstone LIVE (non-member 403 ×2, self-grant 403, member 200, chat at-rest enc:v1:). Pass E — all triggers fired live with content-free copy to the right partner. Pass F — offline cache render + process-death recovery, both 0 FATAL. Pass I — jank 5.25%. Pass J — J-OBS 48dp holds. 0 FATAL across the whole run, both emulators.

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)

  • R15 (2026-06-27) — gap-closing round (Passes L/M/N/P + regression smoke) — found & FIXED M-001 (P2). Build current (HEAD c31eea2 + R15 working-tree changes rebuilt+installed both emulators); baseline both FREE, 0 active sessions. Smoke 6/6 GREEN both (launcher + 5 notif cold-starts). M (settings take-effect)M-001 (P2) quiet hours didn't suppress backgrounded/killed partner pushes (local-only window; OS shows notification block w/o app code). FIXED + verified live: client mirrors window+tz → users/{uid}; 4 partner-action senders suppress via fail-open recipientInQuietHours(); rules allowlist extended. Live: QH ON → fn log is in quiet hours — suppressing, 0 delivery; QH OFF → notified partner. Per-type chat toggle re-confirmed server-enforced (toggle off → 0 delivery; field flips in Firestore). Theme/DataStore persistence across relaunch . Biometric lock code-sound (no compose bypass; observation: re-locks on cold-start, not plain background→resume → Future.md). L (chat E2E) decrypt both dirs, attribution, Seen receipt, ❤️ reaction, ordering, day-sep, inbox no enc: leak, at-rest enc:v1:. N daily-Q + reveal both-answered gate render (outcomes/bucket-list/date-builder carried — render-clean prior rounds). P (content/language) UI copy warm/inclusive, debug rows BuildConfig.DEBUG-gated, friendly error fallbacks; question bank 6103 Qs: 0 empty/0 dupes/0 placeholders/complete answer configs/on-guide tone. D1 at-rest messages/preview/capsules enc:v1:. 0 FATAL. Pass N driven (user "FIX"): N-001 (P1) Bucket List was fully non-functional (coupleId never set → all CRUD no-ops) → FIXED + verified live (add enc:v1: / complete / delete / list render; client-only). N-002 (P2) "Plan a Date"/Date Builder "Create Plan" no-op (wrote to unread prefs collection; dateIdeaId/coupleId never wired) → FIXED + verified live (re-pointed DateBuilderViewModel to create a PLANNED DatePlan via savePlan + resolve coupleId → date_plan status=planned, enc:v1:; Home shows "Date coming up"). Outcomes/Your Progress code-correct (resolves coupleId); daily-Q/reveal render ✓. Uncommitted (user commits): client (BucketListViewModel, DateBuilderViewModel) — M-001's functions/rules/client were committed by the user mid-round (+ user dropped 3 dark-variant PNGs in drawable-night-nodpi/ toward BRAND-DARK-COVERAGE). M-001 functions+rules DEPLOYED to prod; N-001/N-002 are client-only (debug APK installed both emulators). NEXT (R16): confirm M-001 + N-001 + N-002 hold → prune; 2 P3 brand backlogs; revisit Date Builder "both-partners-generate" vision if wanted.
  • R14 (2026-06-27) — full fresh AJ ClaudeQAPlan run (pure QA, no code changes) — FLAWLESS, 0 open P0P3, 0 new findings. Baseline both FREE, 0 active sessions; R13 build on both emulators (5554 dark / 5556 light). A premium enforcement audited (6/6 features gated, not just badged; A-201 class closed) + free→Paywall (Date Match / Desire Sync / Question Packs) + couple-shared unlock (Sam prem→QA free unlocks Desire Sync + a premium pack; modal + subscription_entitlement_changed push delivered live to QA). B Desire Sync + How Well + Spin-the-Wheel full 2-device (True/False+Yes/No+multi-select+free-text answer types; skipped-answer reveal; first-finisher partner_completed_part nudge confirmed in Sam's queue), Memory Lane create+seal (premium), Connection Challenges resume (Day 4 · 🔥2), Date Match deck; ToT carried R13. C broad both-theme + decoupled-theme-art mandate (system-light+app-Dark → dark UI + correctly-themed feathered Today hero); no nav dead-ends (back-from-Home exits = correct). D LIVE non-member 403 ×2 · self-grant 403 · member 200 · chat at-rest enc:v1: (game/capsule at-rest carried R10/R12, crypto unchanged). E all triggers fired live, content-free copy to right partner (started/completed_part/finished + entitlement). F offline Today-from-cache + am kill recovery, 0 FATAL. I jank 5.25%. J J-OBS 48dp holds. 0 FATAL whole run. The 5 R13 fixes held → pruned. Uncommitted (user commits): R13's 16 modified + PremiumUnlockOverlay.kt + illustration_premium_unlock.png (R14 added no code).
  • 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 1 (N-001 Bucket List)
P2 9 (C-THEME-001..009) 2 (M-001 quiet hours, N-002 Date Builder)
P3 2 (BRAND-DARK-COVERAGE, BRAND-ICON-CUSTOM) 0

R15: found + FIXED 3 bugsM-001 (P2 quiet hours), N-001 (P1 Bucket List non-functional), N-002 (P2 Date Builder "Create Plan" no-op) — all verified live, pending 1 confirm. 9 new open P2 theme defects surfaced by scripts/theme-scan.sh (C-THEME-001..009). 2 P3 brand-asset backlogs remain open.

Issues — open (Pass C theme defects + brand-asset backlogs)

Surfaced by the 2026-06-27 brand standards audit (new Pass H/Pass C mandates) and the 2026-06-28 theme-scan run. Brand-quality defects (light-only art, generic icons) and Pass C theme defects (hardcoded surface/background colors) both live here; asset lists + prompts are in ClaudeBrandingReview.md.

ID Sev Area Description Suggested fix Status
N-002 P2 Dates / Date Builder "Plan a Date" / Date Builder "Create Plan" was a no-op. DateBuilderViewModel.savePreference() bailed on state.dateIdeaId.isEmpty() (no entry ever calls setDateIdeaId), built a DatePlanPreference with empty coupleId, and wrote to date_plan_preferences which no screen reads. Net: fill form → Create Plan → nothing saved, no error. Re-point the builder to create a real PLANNED DatePlan via repository.savePlan() (the collection Home already displays via getPlansByStatus(PLANNED)), resolving coupleId from CoupleRepository; dropped the dead dateIdeaId guard. (Product note: this makes the existing single-user form work end-to-end → Home "Date coming up"; the model's older "generate from BOTH partners' prefs" vision is unbuilt — revisit if that's wanted.) Fixed — verified live R15 (Create Plan → date_plan status=planned, enc:v1: duration; Home shows "Date coming up"). Client-only. Pending 1 confirm.
C-THEME-001 P2 Dates / Bucket List AddItemDialog uses a hardcoded light surface. Surface(color = Color.White) in BucketListScreen.kt:406 keeps the dialog background light regardless of the in-app dark theme, producing a light dialog on a dark screen. Replace Color.White with MaterialTheme.colorScheme.surface and ensure text/input colors use onSurface tokens. Open
C-THEME-002 P2 Dates / Bucket List CategoryBadge uses a hardcoded light color. Surface(color = Color(0xFFF3E8FF)) in BucketListScreen.kt:379 renders a light-purple chip in dark mode. Replace with a theme-aware container color (e.g., primaryContainer / surfaceVariant) and ensure label text uses the matching on* token. Open
C-THEME-003 P2 Wheel WheelCompleteScreen uses a hardcoded light background. Box(Modifier.background(Color(0xFFFFFBFE))) in WheelCompleteScreen.kt:507 does not adapt to dark mode. Replace with MaterialTheme.colorScheme.background or route through a themed surface. Open
C-THEME-004 P2 Questions / Discussion thread WaitingPhase banner uses a hardcoded light surface. Surface(color = Color.White.copy(alpha = 0.78f)) in QuestionThreadScreen.kt:168 keeps the "Your answer is saved" banner light in dark mode. Use surfaceVariant or another theme container color with appropriate alpha, or draw the banner fully from theme tokens. Open
C-THEME-005 P2 Wheel / History History locked-state icon uses a hardcoded light surface. Surface(color = Color(0xFFF8F1FF)) in WheelHistoryScreen.kt:356 behind the lock icon does not adapt. Replace with surfaceVariant / surfaceContainerHighest and the lock icon with primary or onSurfaceVariant. Open
C-THEME-006 P2 Components / PlaceholderScreen SignalChip uses a hardcoded light surface. Surface(color = Color.White.copy(alpha = 0.72f)) in PlaceholderScreen.kt:213 plus a hardcoded white border gradient keeps the chip light in dark mode. Replace with surfaceVariant / surfaceContainer and a theme-aware border gradient. Open
C-THEME-007 P2 Components / PlaceholderScreen PreviewPanel uses a hardcoded light surface. Surface(color = Color.White.copy(alpha = 0.78f)) in PlaceholderScreen.kt:243 renders a light panel in dark mode. Replace with surface / surfaceVariant or a theme-aware scrim. Open
C-THEME-008 P2 Dates / Date Match Match CTA chip uses a hardcoded light surface. Surface(color = Color(0xFFF3E8FF)) in DateMatchScreen.kt:240 behind the "View matches" heart icon does not adapt. Use primaryContainer / surfaceVariant and a theme-aware icon tint. Open
C-THEME-009 P2 Dates / Date Match Match badge uses a hardcoded dark-red surface. Surface(color = Color(0xFF8D2D35)) in DateMatchScreen.kt:251 for the match count badge uses a fixed color that doesn't follow the theme. Use error / tertiaryContainer / primaryContainer with matching on* text so it adapts to both themes. Open
N-001 P1 Dates / Bucket List Bucket List was entirely non-functionalsetCoupleId was never called, so coupleId stayed "" and addItem/loadItems/toggleComplete/deleteItem all silently returned. Items could never be added, loaded, completed, or deleted. BucketListViewModel resolves the couple itself in init via CoupleRepository.getCoupleForUser() (mirrors MemoryLaneViewModel), then setCoupleIdloadItems. Fixed — verified live R15 (add persists enc:v1:; complete sets flags; delete removes; list renders). Client-only, no deploy. Pending 1 confirm.
M-001 P2 Settings / notifications Quiet hours did not suppress backgrounded/killed partner pushes. "Quiet hours — 10 PM8 AM, no notifications" was stored local-only (DataStore); partner pushes carry a notification block the OS shows directly when the recipient is backgrounded/killed, and the only client check (PartnerNotificationManager.isInQuietHours) runs foreground-only (AppMessagingService.onMessageReceived). So the "no notifications" promise was broken for the main case. Repro: Sam QH ON @22:28 CST, backgrounded → QA chat → "QA sent a message" posted to Sam's shade. Client mirrors window+tz to users/{uid}; Cloud Functions (onMessageWritten/onAnswerWritten/onAnswerRevealed/onGameSessionUpdate) suppress via fail-open notifications/quietHours.ts:recipientInQuietHours(); firestore.rules user-doc allowlist extended for quietHours*+timezone. Fixed — verified live R15 (fn log suppress vs notify; deployed prod). Pending 1 confirm.
BRAND-DARK-COVERAGE P3 Art / theme Most illustrations are light-only — only 12 of ~25 have a drawable-night-nodpi/ dark variant. All illustration_couple_* heroes (paywall/subscription/onboarding/invite/history), daily_question, partner_activation, tonight_partner_prompt, together_empty, and all 10 pack_art_* banners show the light/pink image on a dark screen (feathered edges don't change the image colors). Generate dark/aubergine-palette variants for each light-only asset → drawable-night-nodpi/ (identical filename); BrandIllustration auto-selects per in-app theme. Re-run the decoupled-theme check. List in ClaudeBrandingReview.md. Open (P3)
BRAND-ICON-CUSTOM P3 Icons / brand ~60 distinct generic Material icons across ~201 call sites (generic hearts Favorite/FavoriteBorder, Person, Lock, Star, PlayArrow, ArrowBack, …) — these are placeholders, not the Closer brand. Replace each with a bespoke glyph_* in the house style (ImageVector.vectorResource + Icon(tint)), highest-traffic first; ship bar = 0 generic Material icons. Backlog table in ClaudeBrandingReview.md. Open (P3)

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-DARK-UI-001 · C-DARK-UI-002 · C-DARK-UI-003 · C-DS-001 · C-ART-EDGE-001 · C-ART-EDGE-002 · 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 · J-OBS — all fixed and re-verified (R14 pruned the 5 R13 fixes — C-DARK-UI-001 ToT dark redesign · C-DARK-UI-002 check-in label/value · C-DARK-UI-003 bottom-inset clearance · C-ART-EDGE-002 8 opaque heroes feathered · J-OBS 48dp touch targets — held through R14's full AJ sweep; in working tree) (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)

  • R14 (2026-06-27) — full fresh AJ run (pure QA, no code), FLAWLESS, 0 new findings. Confirmation round on the R13 build: A premium enforcement audit + couple-shared unlock + entitlement push live; B 3 async games full 2-device + first-finisher nudge + Memory Lane/CC/Date Match core; C decoupled-theme-art mandate; D cornerstone live (403s + enc:v1:); E triggers/copy live; F offline + process-death; I jank 5.25%; J 48dp holds. 0 FATAL both emulators. The 5 R13 fixes held → pruned to the archived line.
  • 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. (Correction R15: the users/{uid} update rule is NOT open — it enforces a field allowlist (firestore.rules ~L198, hasOnly([...])); R15 extended it for quietHours*+timezone. Keep that list in sync with FirestoreUserDataSource when adding a client-written field.)