32 KiB
Claude QA Report — Full-App QA (living report)
Verdict (2026-06-27): R11 confirmation round COMPLETE — FLAWLESS (0 open P0–P2). 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/EmptyStatehelpers, 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 (A–J + fix phase). 0 open P0–P2; 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 (D1–D7). [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)
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 ▶ in progress (Pass B already verified start/first-finisher/finish triggers→correct partners+copy; cold-start tap smoke running bg bjffibz4v) | F–J todo | Admin: scratchpad/qadmin.js + qa/* + scratchpad/d3neg.js (raw-API). Baseline restored (both free, 0 active). | NEXT: confirm smoke 6/6, then Pass F resilience. 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 P0–P2 | 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 (
partFinishNotifiedAtset + Sam queuepartner_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_gameto 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 + 1–5 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 only —
app/.../ui/theme/Theme.kt(LocalAppInDarkTheme CompositionLocal),app/.../ui/components/BrandIllustration.kt(theme-correct-nightvariant via config-overridden context + edge feathering),app/.../ui/components/EmptyState.kt(routes its illustration through BrandIllustration). Everything else (splash fix, E-GAME-003, foreground banner, qa/ tooling) committed by user in2cd0af6. - Foreground "partner started a game" alert + bold Game Waiting card (R10, user-requested) — DONE+VERIFIED. When the app is OPEN and a partner starts a game, a prominent in-app top banner (" started " + Join) slides in (mirrors chat's in-app surface) instead of the easy-to-miss system notification; Join → joins the game. Verified live for all 4 session games: banner name correct (Spin the Wheel / This or That / How Well Do You Know Me / Desire Sync); Join → joins wheel (ToT shows graceful "QA is playing a Wheel game" when types mismatch); suppressed when already on that game's screen (added
ActiveGameSessionMonitor.enter/leavetoWheelSessionViewModel— the others already had it). Home "Game waiting" card redesigned as a bold purple-gradient hero (glyph + game name + "Join the game"), promoted to top of "Waiting for you", verified both themes → tap joins the specific game (not the Play-hub fallback). FCM transport on the emulators is flaky (FcmRetry); the banner was exercised via a data-only high-priority send to the partner token (faithful to the deployed payload). - Pass C progress (R10): Settings family ✅ (dark + Security on light): Settings list, Subscription, Security, Delete account, Notifications all render clean both-relevant-themes; 4 illustrations confirmed in-context (Security padlock, Delete-account doorway, Quiet-hours moon, + Subscription); back-stack OK (Security/Delete/Notif → BACK → Settings). Found C-SEC-001 (P2) — accepter's Recovery phrase disabled + wrong "invite your partner" copy (see Open issues). Wheel back-stack RE-CHECKED = not a trap: live wheel session → BACK → spin/setup → BACK → Play hub (2 backs, no dead-end); earlier "stuck" was automation cycling. Leaving mid-wheel leaves a resumable abandoned active session (normal; cleaned). Home ✅ both themes (stale game card gone).
- 6. Spin the Wheel ✅ — spun→Physical Intimacy→session; Sam joined at Q1; both answered 10 (A/B + 1–5 scale + free-text); per-Q You/QA breakdown renders; completed, 0 active. (Helper
wheel_drive.pyhandles mixed types; free-text Qs hide "Next" behind IME.) - 7. Date Match ✅ — swipe deck ("Swiping with Sam/QA"); QA+Sam mutual like → "It is a match!" modal live; new match persisted (date_matches 3→4); swipe action
enc:v1:at rest (only swipedAt clear). - Pass B = COMPLETE (R10): all 7 games played end-to-end 2-device, 0 bugs. 2 observations: CC day-counter desync (Future.md, by-design?) · WATCH — wheel back-stack: after finishing Spin-the-Wheel, system-BACK from the results re-enters the completed wheel-session screen (loop), needed an app relaunch to escape. Possibly automation artifact (missed taps) — recheck deliberately in Pass C nav fuzzing; file if reproducible (P2 back-stack).
- 5. Memory Lane ✅ — new capsule sealed (3-mo pick) with future open date; title+content
enc:v1:at rest (admin-verified); lists cross-session. Minor cosmetic: "Opens in 2 mo" shown for a 3-month selection (relative-time display nit; not filed). - 4. Connection Challenges ✅ — Gratitude Week (in-progress from R9): per-day step, "I did it today", "waiting for partner" both-gate, missed-day catch-up ("Pick it back up"), streak 🔥→2 synced both devices. UX note (Future.md): "Day N of 7" counter diverges between partners after asymmetric catch-up (QA D4/Sam D3) while streak stays synced — plausibly by-design, non-blocking.
- Pass B progress (R10): 1. This or That ✅ — Deep×10 (varied): QA started, Sam joined via Play-hub card (no duplicate, 1 session), both answered 10, results symmetric both devices ("8/10 in sync", per-Q Match labels correct), session→completed, 0 stale. 2. How Well ✅ — QA-subject 5·Quick: QA answered 5 about self, Sam joined as guesser (asymmetric join works), predicted 5, score+breakdown render correctly (1/5, ✓/✗ guess→actual incl. scale Q), completed, 0 stale.
- R10 scratchpad drivers (reuse):
r10_set_premium.js <QA|Sam> <on|off>·rv_gate.js/rv_markreveal.js(raw-API) ·hw_drive.py <serial> <rounds>(taps first option+Confirm per Q) ·rv_inspect.js/rv_sessions.js(admin reads). Game-option taps: use uiautomator bounds, NOT fixed coords (layouts shift per question; last Q button = "Done →" not "Confirm →"). - Admin writes: user authorized this session (2026-06-26) → premium toggle + baseline reset now working. Baseline reset done (0 active sessions; stale 06-24/06-25 answers cleared). Premium toggle:
scratchpad/r10_set_premium.js <QA|Sam> <on|off>. - Pass A ✅ (R10): neither-premium → Desire Sync shows 🔒 + opens paywall ("Go deeper together"); toggled Sam premium ON → QA(free) Play hub badge cleared live + Desire Sync opens setup (no paywall) = couple-shared unlock holds. Code audit: all gates use
CouplePremiumCheckerexceptSubscriptionScreen(by-design own-status) +DailyQuestionResolver(per-user premium-question fallback — verify in Pass B/E it doesn't desync the couple's daily Q). Other 7 features share the verified path (R9 enumerated each). - Build: HEAD
e6a8dee— clean working tree (reveal feature committed: couple-key encryption, read-gatedsecuresubdoc,onAnswerWrittenboth-answered copy,onAnswerRevealed). Rebuilt + installed on both emulators this session. - Daily-reveal QA (2026-06-26, live, both emulators 5554 dark / 5556 light): Gate (raw API): only-1-answered → partner reads metadata 200 but content 403, non-member 403/403; both-answered → partners read each other 200/200, non-member still 403/403. At-rest: answer doc content-free metadata only; content in gated
secure/payload(enc:v1:). Reveal: shows the partner's answer both directions (the fixed bug) — QA↔Sam. Pushes:onAnswerWrittenfires (both-answered "unlocked ✨" copy is in deployed code);onAnswerRevealedfired live (isRevealedflip → "notified partner that X opened"). 0 FATAL either device. Today's test answers wiped after; baseline clean. One low-sev robustness note →Future.md(revealisRevealedwrite isn't retried if it fails). Note: stale active wheel session + 06-24/06-25 unrevealed answers are pre-existing test pollution (confound the Home dashboard daily card; not the reveal feature). - Devices / accounts: emulator-5554 = QA (
Y05AKO2IlTPMa0JQW1BiNIM0uzK2) · emulator-5556 = Sam (imDjjO…) · paired, coupleIdXal3Kw3gjSdn0niERYKJ, both free (baseline restored). - Docs: Playbook
ClaudeQAPlan.md· CoverageClaudeQACoverage.md· IdeasFuture.md## QA· BrandingClaudeBrandingReview.md.
Severity board
| Severity | Open | Fixed (pending 1 confirm) |
|---|---|---|
| P0 | 0 | 0 |
| P1 | 1 (A-201) | 0 |
| P2 | 0 | 1 (C-DARKART-001) |
| P3 | 2 (J-OBS, C-ART-EDGE-002) | 1 (C-ART-EDGE-001) |
Issues — R12 (1 open P1 [A-201 date-match premium bypass] · 0 open P2 · 1×P2 fixed pending 1 confirm [C-DARKART-001] · 1 open P3 [J-OBS] · 1×P3 fixed pending 1 confirm [C-ART-EDGE-001])
R11 fixed the two open art issues in the shared
BrandIllustration/EmptyStatehelpers and verified both live on both decoupled theme directions, 0 FATAL. The 5 R10 P2 fixes were re-confirmed this round and pruned to the archived-ID line below (detail in git9c84c36). Fixes for the two art issues are in the working tree (user commits). Fix summary: C-DARKART-001 —LocalAppInDarkThemeCompositionLocal (set inCloserTheme) drives a config-overridden context (createConfigurationContextwithUI_MODE_NIGHT_*from the in-app theme) so-nightart follows the app's own theme, not the system. C-ART-EDGE-001 — tiled art feathers its 4 edges to transparent (graphicsLayer{Offscreen}+drawWithContentBlendMode.DstInlinear gradients) instead of a hardclip+border;EmptyStatenow routes its illustration throughBrandIllustrationso both fixes apply everywhere from one place.
| 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. |
Open (P1) |
| C-DARKART-001 | P2 | Theme / dark-mode art (Pass C) | Dark-mode illustrations didn't follow the IN-APP theme switch — only the system dark mode. The in-app toggle (Settings → Appearance → Dark) swapped Compose colors via CloserTheme(darkTheme=…) but had no config uiMode override, so painterResource + the drawable-night-nodpi/ variants resolved off the system uiMode → app-Dark on a light-mode phone showed dark UI + light illustrations. Affected all 12 -night illustrations. |
5554: cmd uimode night no (system light) → Settings → Appearance → Dark → before fix Security showed the light padlock tile on a dark screen. |
DONE: BrandIllustration loads the drawable through context.createConfigurationContext(cfg) with UI_MODE_NIGHT_* set from LocalAppInDarkTheme (provided in CloserTheme). Verified live R11 both directions: 5554 system-light + app-Dark → dark aubergine art on dark screen (Security + Art-preview gallery); 5556 system-dark + app-Light → light pastel art on light screen; 0 FATAL, both apps alive. |
Fixed + verified live R11 (working tree; user commits) |
| C-ART-EDGE-001 | P3 | Art / edge treatment (Pass C+H) | Displayed illustrations had hard edges instead of fading into the screen — BrandIllustration hard-clipped art to RoundedCornerShape + a hairline border, and EmptyState rendered raw painterResource, so the near-white tile read as a crisp rounded-rectangle boundary (esp. on dark). |
Any art screen: hard tile edge/outline instead of feathering. | DONE: tiled art now feathers its 4 edges to transparent (graphicsLayer{compositingStrategy=Offscreen} + drawWithContent BlendMode.DstIn linear gradients, ~14% inset); clip+border removed; EmptyState routes through BrandIllustration. Verified live R11: Art-preview gallery + Security padlock melt softly into the surface on both themes; transparent art (tile=false) unaffected. |
Fixed + verified live R11 (working tree; user commits) |
| 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. |
Open (P3) |
| J-OBS | P3 | A11y / touch targets | A few conversation icon-buttons measure ~42–45dp wide (48dp tall) — single-axis marginal miss of the 48dp target; fully operable. Most controls are 48dp. | Pass J: uiautomator bounds on conversation → 2–3 clickables <126px wide. |
Bump those icon-buttons to 48dp min (e.g. Modifier.minimumInteractiveComponentSize() / size(48.dp)). |
Open (P3, non-blocking) |
Resolved & confirmed (archived — full detail in git history)
A-001 · A-003 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DS-001 · C-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 (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/premiumdenied (server-only → no self-grant); cross-couple denied. - D4 keys: couple key phrase-wrapped (argon2id); recovery phrase server-blind;
encryptedRecoveryPhrasewiped on acceptance; plaintextinviteCodenot exploitable (invite readable only by inviter; no code-encrypted secret persisted). - Robustness: malformed/abusive deep-link intents (unknown type, missing extras, injection/path-traversal) → 0 crash; killed-state cold-start chat deep-link → conversation loads.
⛔ "All notifications broken / app opens-and-closes" — ROOT CAUSE = splash crash (FIXED R10)
The actual cause was NOT routing — it was a crash in the splash-screen exit animation on notification cold-starts.
MainActivity.onCreate (added in 95cad84, 2026-06-25) set splashScreen.setOnExitAnimationListener { provider -> provider.iconView.animate()… }.
On a notification / PendingIntent cold-start the OS hands the splash view over without an icon (SplashScreenView: Icon: view: null),
and provider.iconView throws an internal NullPointerException (SplashScreenViewProvider$ViewImpl31.getIconView) →
onCreate crashes → "Force finishing activity" → the app opened and immediately closed on EVERY notification tap
(chat, game-start, results — all of them, because they share the cold-start path). This is why it looked like "all
notifications broke again." Normal launcher cold-starts were fine (icon present), which masked it.
- Why my earlier
am starttests missed it: shellam startuses a different splash transfer than the FCM PendingIntent handover (the SysUILaunch remote transition), so it didn't hit the null-icon handover. Alsoam force-stopcan't receive FCM at all (stopped-package broadcast exclusion) — must useam killto test killed-app push. - Fix (R10, working tree):
MainActivitywraps the icon scale inrunCatching(best-effort) and the view fade inrunCatching { … }.onFailure { provider.remove() }so the splash is always removed and onCreate never crashes. - Verified live: real FCM notification → killed (
am kill) Closer2 → tapped the OS notification → cold-start logsIcon: view: nullthenremove starting view, 0 FATAL, process stays alive, lands on Home (was the crash). Normal launcher cold-start still animates + works.
Notification deep-link routing — SINGLE mechanism (do NOT reintroduce a second one)
Invariant: an app-posted notification carries the resolved route in one place — the app_route extra —
and routing is MainActivity.deepLinkRouteFromIntent → pendingDeepLink → AppNavigation navigateRoute. Do not
also set an ACTION_VIEW + closer:// data Uri on the notification intent: for routes that have a navDeepLink
(conversation / answer_reveal / daily_question / question_thread / home / play) the NavController auto-handles that Uri
in addition to pendingDeepLink → a race/duplicate nav. That dual path is what kept re-breaking notifications.
- Why it broke "again" (root cause, traced via git):
aaab768/1b9d8cf/b9b1560built routing on thecloser://data Uri (NavController auto-handle) + apendingDeepLinkgated oncurrentRoute == HOME; then38fdc6dadded theapp_routeextra on top without removing the data Uri → two mechanisms for the same tap. The HOME-only gate also meant a warm tap from any non-Home tab setpendingDeepLinkbut never consumed it. - Fix (R10, working tree):
PartnerNotificationManager.showNotificationno longer setsACTION_VIEW/data Uri —app_routeextra only.AppNavigationpendingDeepLink gate broadened from== HOMEto!in entryRoutes(fires once past onboarding, on any main screen). Verified live (0 FATAL): killed-app tap → chat opens the conversation; all 4 game results pushes (partner_finished_game) load the real per-session results (wheel "Here's how you each answered" · This-or-That "5/5 in sync" · How Well "Perfect read 5/5" · Desire Sync "5 shared desires"); app_route-only path (no Uri) loads results; warm tap from Settings now routes (was the stuck case).
Round history (one line each)
- R11 (2026-06-27) — confirmation round, FLAWLESS (0 open P0–P2). Fixed the last open P2 C-DARKART-001 (dark-mode
art now follows the in-app theme:
LocalAppInDarkThemeCompositionLocal inCloserTheme→BrandIllustrationloads the-nightdrawable via acreateConfigurationContextwhoseUI_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 viagraphicsLayer{Offscreen}+BlendMode.DstIngradients instead of hardclip+border;EmptyStatenow routes throughBrandIllustration). 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-backpopUpTopresent · 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).
- notification cold-starts open & stay — splash-crash class clean). Art fixes in working tree; everything else committed
(
- E-GAME-003 (2026-06-27) — FIXED+VERIFIED+DEPLOYED: async-game first-finisher left the waiting partner un-notified.
Async games (this_or_that/wheel/how_well/desire_sync) write answers to
couples/{c}/{gameType}/{sessionId}and the session only flips tocompletedwhen BOTH answer — soonGameSessionUpdate(watches the session doc) never fired on a single finish, and the waiting partner got nothing ("Closer2 finished a game but the partner was never notified"). Fix = new Cloud FunctiononGamePartFinished(trigger on the answer doc; on exactly-1 answer, idempotently claimpartFinishNotifiedAton the session + sendpartner_completed_part"X finished their part — your turn to play!"). Verified live: QA finished ToT part → sessionpartFinishNotifiedAt=true, Sam queue got 1partner_completed_part, posted on Sam's device, tap → opened ToT, 0 FATAL. Deployed (onGamePartFinishedcreated,onGameSessionUpdateupdated). Funcs source uncommitted (user commits). - R10 (2026-06-26) — FULL ClaudeQAPlan run A–J + fix phase. Found 5 P2 in report-only passes, fixed + verified all live: C-HOME-001 (Home dup pending card), C-NAV-002 (wheel results→BACK re-entered finished session), C-NAV-003 (duplicate app bar on Wheel History/PartnerHome), C-PW-001 (dark paywall pills light-on-light), C-SEC-001 (Security read wrong recovery-phrase store → accepter couldn't view phrase; E2EE recovery itself sound). E-GAME-002 confirmed live (startNotifiedAt set + partner_started_game→right partner + foreground banner + Join→joined active ToT) → pruned. D1–D7 security clean (non-member denied all raw-API reads/writes, no self-grant, secure-subdoc gate correct, argon2id+AAD=coupleId). Concurrency double-start→1 session. Perf jank 5.53% / a11y font-2.0 reflows — no regression. Build OK, both emulators reinstalled, 0 FATAL, content still
enc:v1:. App fixes in working tree (user commits). - Notif→game fix + dark art + QA sweep (2026-06-26, uncommitted). E-GAME-001 (P1, FIXED+VERIFIED): game notifications "led nowhere" — backgrounded/warm taps landed on Home (MainActivity was standard launch mode →
onNewIntentnever delivered the tap's extras →pendingDeepLinkunset), and even when routed, the game screen showed setup instead of joining (one-shotgetActiveSessionForCoupleraced the post-push Firestore sync → returned stale-empty). Fixes:AndroidManifestMainActivity launchMode=singleTop+QuestionSessionRepositoryImpl.getActiveSessionForCouplenow SERVER-first (cache fallback). Verified live: Sam backgrounded → taps partner_started_game → lands IN the active This-or-That (1/10), joined, no duplicate session; back-stack sane (game→back→Home→back→exit, C-NAV-001 holds). Generic across game types (shared routing + getActiveSession). Dark-theme art: 12_darkvariants →drawable-night-nodpi/(light names) so dark mode auto-swaps; verified live (Security shows the aubergine variant on dark; light unchanged). QA sweep: tabs both themes, deep-link back-stack, all 12 illustrations both themes — 0 FATAL, baseline intact. - Brand art drop (2026-06-26) — wired + QA-swept, 0 issues. All 11 generated illustrations (A1–A12, source gitignored) wired into their screens via shared
EmptyState+ newBrandIllustrationhelper (commits077a408→5868d06). Complete both-theme sweep: in-context dark and light verified for Bucket List (A6), Quiet hours (A9), Security (A11), Delete account (A12) — all render as crisp rounded tiles, on-brand, no clipping/contrast issue; A1 (transparent), A3 (banner) + the empty-only states (A2/A4/A5/A8/A10, unreachable on the baseline couple) verified via the debug Art-preview gallery on both themes + the proven shared tile. 0 FATAL/ANR both devices; baseline intact (0 sessions/outcomes). Process catch: 5556 was on a stale build mid-sweep → reinstalled current, both now on768f511. Details inClaudeBrandingReview.md. - R9 — clean confirmation round (0 new findings): confirmed + pruned I-001/I-002 (0 outcomes denials/CCE on the fixed build); swept deferred Pass C deep/list screens (Answer History, Activity, Bucket List, Date Match/Matches — both themes) + Pass F network (offline cache render + clean reconnect). 0 open P0–P2.
- R8 — F-RACE-001 re-confirmed + pruned; Passes I (perf) + J (a11y) run; found+fixed+verified I-001 & I-002 (outcomes read: query rules-denied + Long/Int parse CCE → "Your Progress" was silently dead). 0 open P0–P2.
- R7 — multi-angle security/concurrency deep dive → cornerstone fully clean; F-RACE-001 found + fixed + verified. 0 new open.
- R6 — branding drop + Future.md backlog regression (white-keyhole icons/loader/splash, inclusive gender, copy, rate-limit split, results-push suppression, paywall retry/offline) → 0 new open.
- R5 — Cloud Functions deployed (E-OBS channel fix, E-003 results routing) + new Pass G (account creation / fake-account abuse) clean → 0 open.
- R1–R4 — baseline Passes A–F report-only; every P0–P2 found was fixed + verified (see archived IDs).
Operational constants
- Execution mode: autonomous run-to-completion — don't stop; fix blockers inline; cycle fix→re-QA until flawless. Don't hand back when context fills — re-read this run-state + coverage after any compaction. Commit before interruptible work; recover stuck sessions via the session-start ritual.
- Standing authorization (user, 2026-06-24): may
firebase deploy --only firestore:rules+ has admin access (Firestore reads/writes/seeds + entitlement toggles) — run without pausing. Only the macOS requirement for iOS (Parts 2/3) is a hard stop. - Hardening backlog → Future.md: App Check not enforced on Firestore;
users/{uid}update rule allows arbitrary non-hasPremiumfields (tighten to a field allowlist).