diff --git a/ClaudeQAPlan.md b/ClaudeQAPlan.md index 3d4f97d3..f6c57c67 100644 --- a/ClaudeQAPlan.md +++ b/ClaudeQAPlan.md @@ -29,8 +29,12 @@ process rule, make sure it doesn't contradict the Guardrails. both emulators; **installed build == HEAD** (rebuild+install if unsure — never QA a stale APK); baseline clean (both free, 0 active sessions, logcat 0 FATAL). 2. **Discovery ritual:** reconcile routes/notifications/features/assets/backend with coverage; fold new surfaces in. -3. **Run the scanners FIRST (cheap, before live driving):** `qa/entrypoint_smoke.sh` (both serials), `scripts/theme-scan.sh` - (Pass C), `scripts/wiring-scan.sh` (Pass N). File 🔴/🟠 to `ClaudeReport.md`; record counts in coverage. +3. **Run the cheap gates FIRST (before live driving):** (a) the **automated test suites** — `./gradlew testDebugUnitTest` + + `cd functions && npm test` (they cover the fragile logic: encryption format, rate limiter, quiet hours, streak, + entitlement math) — **a red suite is a P0/P1 regression gate, stop and fix before QA'ing a build**; (b) the **scanners** — + `qa/entrypoint_smoke.sh` (both serials), `scripts/theme-scan.sh` (Pass C), `scripts/wiring-scan.sh` (Pass N); (c) optional + **monkey fuzz** `adb shell monkey -p app.closer --throttle 300 --pct-touch 90 -v 5000` (any crash = bug). File 🔴/🟠 to + `ClaudeReport.md`; record counts in coverage. 4. **Run the passes report-only**, sub-batched to one context window each — recurring set **A–N + P** (K money-path + O release gates only when a sandbox device / pre-ship is in scope). Checkpoint the MD files after each chunk. 5. **Fix phase** (after all passes): by severity P0→P1→P2→P3, one at a time, verify each live via the **real path** + @@ -206,6 +210,20 @@ confirms + enumerates this; the fix phase applies couple-shared everywhere. splash-crash class that `am start` can't. Run it **every round and after any change touching MainActivity / splash / theme / manifest / nav / notifications**. `FAIL` = an app crash (real bug); `BLOCK` = push not delivered (flaky emulator FCM — rerun, not a bug). +- **Run the project's OWN test suites every round (they are the cheapest, most deterministic regression net).** Before + the scanners and live driving, run `./gradlew testDebugUnitTest` (19 unit tests — `FieldEncryptorTest`, + `SealedAnswerEncryptorTest`, `NotificationRateLimiterTest`, `QuietHoursManagerTest`, `StreakCalculatorTest`, + `ChallengeStateMachineTest`, `PartnerNotificationManagerTest`, `HomePriorityEngineTest`, `DateMatchRepositoryImplTest`, + `CloserBrandCopyTest`, …) and `cd functions && npm test` (`entitlementLogic.test.ts`). **A failing test is a regression + bug (P0/P1) — file it and do not QA a build with a red suite.** A fix that breaks a test isn't "Fixed" (see Fix phase). + These guard the exact invariants this QA chases (ciphertext format, rate limiting, quiet-hours suppression, + entitlement math), so a green run is a precondition, not a bonus. (**Coverage gap:** there are **0 instrumented + tests** — no on-device Compose/Espresso smoke; logged in `Future.md`. Until that exists, the live passes + scanners + are the only UI-behavior net.) +- **Stress / monkey fuzz (cheap random-crash net the manual nav-fuzz misses):** once per build run + `adb shell monkey -p app.closer --throttle 300 --pct-touch 90 -v 5000` on each emulator with `logcat` capturing — + any `FATAL EXCEPTION`/ANR it triggers is a bug (file it with the monkey seed). This complements Pass C's *targeted* + nav fuzzing with broad random input. - **Run associated automated scanners BEFORE the manual pass.** Every pass with a supporting script must start with it: - **Pass C:** run `scripts/theme-scan.sh` and review `/tmp/claude-theme-scan-.md` before looking at any screen. - **Pass N (+ discovery ritual):** run `scripts/wiring-scan.sh` and review `/tmp/claude-wiring-scan-.md` before @@ -238,7 +256,10 @@ confirms + enumerates this; the fix phase applies couple-shared everywhere. fails" ⇒ it's a **crash until the stack says otherwise** — `logcat -c` then capture `FATAL EXCEPTION` from the live repro **before** proposing a cause (don't fix by reasoning, like the routing red-herring on this very bug). (2) **Many features break at once ⇒ inspect the SHARED code path** (launch/`onCreate`/splash/auth/key-load), not each - feature. (3) "worked before, broken now" ⇒ `git blame`/`git log -L` the failing line to the introducing commit. (4) + feature. (3) "worked before, broken now" ⇒ **diff & history-check before you fix**: `git blame`/`git log -L`/`git diff` + the failing line to the introducing commit (**incl. other agents' commits — Codex/kimi/Ripley co-edit this repo**), and + search the Engineering Manual landmines + the report's archived-ID line for a prior fix of the same symptom — a match + means **regression, not a new bug** (full procedure: the Fix-phase **Regression triage** step). (4) Treat cosmetic/branding/theme/manifest/splash commits as **capable of deep crashes** — re-run the cold-start + notification smoke after them. @@ -603,6 +624,13 @@ Account); Paywall; Your Progress/Activity; Recovery. - **Readability at scale:** default font size + spot-check largest system font scale on text-heavy screens. (The full accessibility sweep — large-font on every primary flow, TalkBack labels, touch targets, keyboard, reduce-motion — is **Pass J**; per-route performance/jank is **Pass I**.) +- **Orientation / form-factor (the app is NOT portrait-locked — `AndroidManifest.xml` declares no `screenOrientation`, so + it DOES rotate to landscape).** Don't only check "rotation doesn't lose state" (that's Pass F) — verify the **landscape + layout actually renders correctly** on the text-heavy / game / paywall / dialog screens (`adb shell settings put system + accelerometer_rotation 1` then rotate, or use the emulator rotate control): no clipped/cut-off content, no broken + scrolling, dialogs and bottom CTAs still reachable. Spot-check a **large-screen / tablet** AVD too. If landscape is not + a supported experience, the correct fix is to **lock portrait in the manifest** — file that as the finding (see the + app-finding note) rather than shipping an unverified landscape layout. - **Navigation from every entry point:** reach each screen from **all** the places that link to it and confirm it opens correctly each time — e.g. a conversation from the inbox AND from "Discuss" AND from a notification; a game from the Play hub AND from a notification; Paywall from each gated feature; Settings sub-pages; reveal from Today @@ -669,6 +697,17 @@ Account); Paywall; Your Progress/Activity; Recovery. (KDF from invite seed; server holds only `wrappedCoupleKey`+`kdfSalt`/`kdfParams`+`encryptedRecoveryPhrase`); **KDF strength**; Tink AEAD = AES-GCM/256 with **AAD=coupleId**, no weak/custom crypto/nonce reuse; keybox/sealed/commitment integrity; **recovery-wrap server-blind**; **unpair revokes decrypt**; invites CSPRNG + single-use + expiry. + - **NEW-DEVICE / LOST-PHONE RECOVERY — drive it end-to-end, don't just verify the phrase is revealed (the make-or-break + data-continuity path for an E2E app).** The keys are single-device (a known limitation); the recovery phrase is the + only bridge. Infra: `crypto/RecoveryKeyManager.kt`, `data/local/RecoveryPhraseStore.kt`, `ui/pairing/RecoveryViewModel.kt`, + `crypto/CoupleKeyStore.kt`. Exercise the **full flow on a fresh install / second device**: sign in → enter the + recovery phrase → the couple key is rebuilt → **prior `enc:v1:`/`sealed:v1:` messages and answers actually DECRYPT + and render** (not just new ones). Then the failure paths: a **wrong/typo'd phrase** fails gracefully (clear error, no + crash, no corruption); a user who **lost the phrase** is told honestly what is/isn't recoverable; and throughout, the + **partner's** device keeps working (one side recovering must never break the other). Confirm the server stayed blind + (only `wrappedCoupleKey`/`encryptedRecoveryPhrase` ever transit — verify via admin read). Without this, "I got a new + phone" silently loses the relationship history. (Also exercised from the account-lifecycle angle in Pass F and the + Settings → Security flow in Pass M.) - **D5 App Check / Functions / secrets:** App Check enforced; callables validate auth+membership; webhook authenticity; admin-only writes rejected from clients; service-account JSONs never committed; no plaintext/secrets in logcat; temp files deleted. @@ -767,6 +806,17 @@ open**. No duplicates; rate limiter (20/day, 100/week) doesn't drop legit ones. partner/account switch; stale token cleanup; app reinstall/update; and notification channel migration. Denied/system disabled notifications should fail gracefully with in-app state still correct, never with lost data or broken routing after permission is restored. +- **Doze / battery-optimization / background-restriction delivery (real-device gate — emulators NEVER enter these states, + so per-round emulator passes systematically miss the #1 real-world "notifications don't work" cause).** Scheduling is + entirely server-side (no client `WorkManager`), so the only thing standing between a fired push and the user is the OS + power state. On a **physical device**, verify each push type still delivers when the recipient device is dozing / + the app is battery-optimized or "Restricted": `adb shell dumpsys deviceidle force-idle` (then send a real partner + action + a scheduled push), app set to **Optimized** then **Restricted** in battery settings, and App Standby buckets. + Assert: high-priority FCM (partner actions) wakes the device and delivers; lower-priority/data-only pushes degrade + *predictably* (document which); scheduled pushes (daily question, capsule unlock, reminders) still arrive within the + expected window. Because our recurring setup is two emulators, keep this as a `blocked→needs-device` row in + `ClaudeQACoverage.md` (with the device-matrix gate) rather than silently assuming delivery — and run it before any + store push. OEM battery-killers (Xiaomi/Samsung/etc.) are even more aggressive; note them for the device matrix. - **Six assertions per notification:** (1) trigger fires correctly — right event, not early, not twice, sender doesn't get their own (unless intended), retry/idempotency doesn't duplicate; (2) delivered to the right person — correct token, old tokens unused after sign-out/account-switch; (3) copy + channel correct — friendly, right channel/ @@ -834,6 +884,17 @@ open**. No duplicates; rate limiter (20/day, 100/week) doesn't drop legit ones. - **Lifecycle / process death:** background mid-flow + return; force-kill the app and relaunch (Android may kill the process) — state/auth/draft restore sanely; deep-link/notification after process death still loads (verified for chat — extend to all). Rotation/config-change doesn't lose Compose state. Low-memory. + - **Deterministic state-restoration ("Don't keep activities" — do NOT rely only on `am kill`).** `am kill` is + non-deterministic; enable **Developer options → Don't keep activities** (`adb shell settings put global + always_finish_activities 1`) so the Activity/process is destroyed on *every* backgrounding, then walk each primary + flow (sign-up, pairing, a game mid-answer, an unsent message draft, capsule/Date Builder in progress, paywall) and + background→return at each step. Assert **no lost** form input, scroll position, draft, in-progress game state, or + nav back-stack — i.e. `rememberSaveable`/`SavedStateHandle` actually persist it. Restore with + `adb shell settings put global always_finish_activities 0` after. +- **Interruptions mid-flow (the OS or another app steals focus):** incoming phone call, alarm, another app taking the + foreground, screen-off/on, **split-screen / multi-window**, and picture-in-picture during a game/answer/message-compose + → returning resumes cleanly with no lost state, no crash, no duplicate submit, and audio/camera (voice note, photo) + releases + re-acquires sanely. - **Cold-start launch integrity from EVERY entry point (Pass F OWNS this — it's the shared path no other pass owned, and where the splash-crash hid):** the app must **open AND stay** (no crash, no "opens-and-closes", lands off the launcher) when cold-started from: the **launcher icon**, **each notification type tapped from a killed (`am kill`) app**, a @@ -916,6 +977,12 @@ reads, missing cache use, and slow navigation. Drive each route as a user and in - **Redundant Firestore / network reads:** count listeners/gets per screen. Switching bottom tabs and returning must **not** refetch unchanged data; opening a screen twice must not double-read; **snapshot listeners detach on leave** (no leaked/stacked listeners — a 2-user realtime app accumulates these fast). Watch for N+1 reads on lists. +- **Memory leaks (beyond listener leaks):** add **LeakCanary** in the debug build (or take heap dumps) and navigate + in→out of every heavy screen (conversation with media, game, image viewer, Memory Lane) repeatedly — flag retained + Activities/Composables/bitmaps/Contexts. A leak that grows per navigation = bug (P2; **P1** if it OOMs). +- **StrictMode in debug (catch main-thread I/O + leaked closeables cheaply):** enable a `StrictMode` thread + VM policy + in the debug `Application` (`detectDiskReads/Writes/Network`, `detectLeakedClosableObjects`); any violation logged on a + primary flow is a finding (disk/network on the main thread → jank/ANR risk). - **Caching / lazy-load:** static question/category data is cached locally (Room) and not re-fetched each entry; large lists use lazy paging (`LazyColumn`/paging, not load-all); images cached (Coil); offline reads serve from cache. - **Latency:** measure cold-start-to-interactive (splash→loader→Home) and tab/route transition latency; flag anything @@ -940,6 +1007,11 @@ This is the deep home for a11y; the Pass C contrast/font spot-checks feed into i date swipe deck, answer cards) are operable + announced; no focus traps. - **Contrast:** body text + essential icons meet WCAG AA (4.5:1 body / 3:1 large) in **both** themes — measure, don't eyeball; re-check the known dim spots (game answer text, muted captions, the C-DS-001 area). +- **Don't rely on color alone (color-blind / WCAG 1.4.1):** any state conveyed by color must also carry a non-color cue + (icon, label, shape, position). Audit the **match/mismatch** rendering (e.g. `AnswerRevealScreen`), status chips, + selected/disabled states, and any red=bad/green=good signal — they must be distinguishable in grayscale / with a + color-blindness simulation (`adb shell settings put secure accessibility_display_daltonizer_enabled 1`). Color-only + status = bug. - **Touch targets:** interactive targets ≥ **48dp** (icon buttons, chips, nav, close/back, reaction buttons, swipe-deck actions). Flag anything smaller. - **Keyboard / external input:** with a hardware keyboard, forms (sign-up, message, capsule, profile) tab in a sane @@ -1031,7 +1103,17 @@ takes real effect. Read [Authentication and pairing flow](docs/Engineering_Refer - **Delete account** → confirmation → account + couple data cascade (`onUserDelete`), partner unpaired + notified, re-create with the same email is a clean slate (overlaps G). - **Security** → recovery-phrase reveal for **both** accepter and inviter (C-SEC-001), server-blind (D4); regenerate if - supported. **Subscription** → "Manage subscription" → Play (Pass K). **Privacy & Terms / data export** links open. + supported; **and the full new-device recovery flow — enter the phrase on a fresh install → existing history decrypts** + (canonical steps + failure paths in **D4**). **Subscription** → "Manage subscription" → Play (Pass K). **Privacy & + Terms / data export** links open (the export *contents* are verified in Pass O). +- **Analytics / funnel-event correctness (not just the leak check in D6).** The app ships a real analytics tracker + (`core/analytics/FirebaseAnalyticsTracker.kt`, wired via `di/ObservabilityModule.kt`); D6 only asserts *no private + content* leaks into it — nobody verifies the events actually **fire correctly**, so the business funnel can silently + break. Enable Firebase **DebugView** (`adb shell setprop debug.firebase.analytics.app app.closer`) and confirm the key + lifecycle events fire **once, at the right moment, with correct params**: signup, pair, paywall_view, purchase/restore + (Pass K), game_complete, daily_answer/reveal. Also confirm analytics honor any **consent / opt-out** (privacy): if a + toggle or first-run consent exists, opting out must actually stop collection. Wrong/missing/duplicated events = bug; + still no private content in any event (D6). - Every toggle survives **process death + reinstall-with-data** (overlaps F). ### Pass N — Daily question, reveal, check-ins & the other interactive features @@ -1080,13 +1162,24 @@ changes). - **Deep links / Android App Links:** `closer://` **and** any `https` App Links (`assetlinks.json`) open the correct screen with auth/membership re-checked (overlaps E). - **Permissions & manifest:** the manifest declares only what's used; runtime prompts (POST_NOTIFICATIONS, camera, mic, - Android-13+ photo picker / `READ_MEDIA_IMAGES`) appear and degrade gracefully when denied; `allowBackup=false` holds (D6). + Android-13+ photo picker / `READ_MEDIA_IMAGES`) appear and degrade gracefully when denied; `allowBackup=false` holds (D6); + and the **`screenOrientation` decision is explicit** — today the manifest sets none, so the app rotates to an unverified + landscape layout (Pass C). Either lock portrait or certify landscape; don't ship it undecided. +- **Age gate / content rating / maturity (the app has adult/intimacy content — Desire Sync — and currently NO age gate).** + Confirm an appropriate **18+ / age-appropriate gate** exists where required, the Play **content/maturity rating + questionnaire** matches the actual content, and any IAP/intimacy content complies with store policy. A missing age gate + on adult content is a **store-rejection + legal risk** — file it (see the app-finding note). - **Localization & formats (i18n):** strings are externalized (no hardcoded user-facing text), the longest translations don't clip (overlaps C/J), **RTL** mirrors correctly, dates/numbers/**subscription prices+currency** format per locale (overlaps K). Even if English-only today, confirm there's no layout that assumes English length. - **Play Store readiness:** the **Data Safety** form matches the actual data flows + E2E encryption; privacy-policy URL live; version code/name bumped; store listing/screenshots are the brand pass (H); min/target-SDK **device matrix** (Methodology) covered. +- **Data-rights compliance (GDPR/CCPA — verify the CONTENTS, not just that a link opens).** Pass M confirms the export / + privacy links resolve; here, confirm **right-to-access** actually returns the user's real data (and that E2E content is + handled correctly — exported decrypted to the owner, or documented as unrecoverable), and **right-to-erasure** (delete + account, Pass M/G) genuinely cascades server-side (`onUserDelete`). A privacy policy that claims flows the app doesn't + do (or omits ones it does) is a finding. ### Pass P — Content, copy & language quality (voice, grammar, inclusivity, the question bank) **Wrong language is a BUG, not a "nice-to-have."** Typos, grammar/punctuation errors, off-brand or cold/salesy voice, @@ -1141,6 +1234,12 @@ fixed. Stale fixed rows and stacked old run-states make it unreadable and hide t (the row cites the landmine ID, and the commit hash once the user commits), so nothing is lost. Don't rely on "the commit message" as the only home — **you don't commit** (the user does, often batched), so the manual landmine is the reliable record. Don't carry confirmed-fixed issues across multiple rounds. + - **Make the archived-ID line a usable duplicate-fix lookup, not bare IDs.** When you prune a row, attach a **2–4 word + tag** to its archived ID (e.g. `C-PW-001 dark paywall pills`) so the Fix-phase **Regression triage** can search it by + symptom. **Any fix whose class could plausibly recur gets at least a one-line Engineering Manual landmine entry** — + not only the "escaped/deep" bugs the MANDATORY-retrospective requires — so a future regression check never lands on an + ID with no description. (This is why a separate fix-history file is unnecessary: the manual landmines + this tagged + archived line + git already are the fix history.) - **One run-state header, always.** Keep only the **current** `Round N | Pass X | Chunk Y | NEXT ACTION` block pinned at the top. Don't stack prior rounds' headers — collapse finished rounds into at most a **single one-line history** entry each (e.g. `R6: branding regression — 0 new`), or drop them entirely once their fixes are confirmed-and-pruned. @@ -1175,9 +1274,25 @@ Optimize every QA doc for a reader who has **5 seconds** to find the current sta ## Fix phase (only AFTER all passes of the round complete) - Work strictly by severity: **all P0 → P1 → P2 → P3**. +- **⛔ Regression triage — DIFF & history-check BEFORE you write a fix (every bug, not just crashes — don't fix blind).** + First answer *"is this NEW, or did we break/relapse something that worked?"* — fixing without this risks re-fixing a + known issue a different way (divergent fixes) or masking the real regression: + 1. **Have we fixed this before? (duplicate-fix / regression check.)** Search the **fix history** for the same + symptom/area/ID — the canonical home is the Engineering Manual + [Known landmines and recent fixes](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes) (root + cause + the guard that should hold it) plus `ClaudeReport.md`'s `Resolved & confirmed` archived-ID line. **A match ⇒ + this is a REGRESSION, not a new bug:** re-open under the **original ID**, and fix *why the guard lapsed* (a scanner/ + test/pass-step that was supposed to catch it) — do **not** re-implement a fresh fix from scratch. + 2. **What changed? (diff before you fix.)** `git log` / `git diff` / `git blame` / `git log -L` the failing area to pin + the introducing change — **including OTHER agents' recent commits** (this repo is co-edited by Codex / kimi / Ripley, + so "what changed" is frequently not your own work; `git log --since` / `git log ` across authors). Read that + commit's diff and fix the **actual cause it introduced**, not the surface symptom. ("worked before, broken now" + ⇒ always bisect to the change first.) + 3. Only after you know **new-vs-regression** and **what introduced it** do you design the fix. - **One issue at a time**: implement → `./gradlew :app:assembleDebug` → install both → verify THAT fix live (correct device/theme) + regression smoke (launch/no-crash, send text, inbox loads, a game opens, **content still ciphertext - in Firestore**) → flip its row to **Fixed** + capture the durable substance in the Engineering Manual landmine → next + in Firestore**, **`./gradlew testDebugUnitTest` + functions `npm test` still green** — a fix that reds a test isn't + Fixed) → flip its row to **Fixed** + capture the durable substance in the Engineering Manual landmine → next (the **user** commits per issue/cluster — never run git yourself; see Guardrails). Don't start the next until the current is verified. - **Real-path verification gate (do NOT mark Fixed without it):** verify the fix through the **same path the user hits**, @@ -1203,7 +1318,8 @@ every notification type verified or explicitly `not implemented→Future.md`, ch (A) + settings-take-effect (M) + **interactive features (N: daily-Q/reveal, outcomes, Bucket List, Date Builder work end-to-end — created data persists AND is read back, `scripts/wiring-scan.sh` 🔴=0)** + content/language (P: no typos/ off-voice/non-inclusive copy, question bank on-guide) verified, all join-game navigation paths and all back-stack checks -verified**, **and `qa/entrypoint_smoke.sh` GREEN on +verified**, **the unit + functions test suites GREEN (`./gradlew testDebugUnitTest` + functions `npm test`)**, **and +`qa/entrypoint_smoke.sh` GREEN on both emulators (0 FAIL — every entry-point cold-start opens and stays)**. Then stop (P3s optional). **Pass O (release build + store readiness) and Pass K's real-money path are pre-ship / real-device gates** — they don't block a per-round "flawless" but **must be GREEN before any store submission**. Don't re-open a clean pass within the same round. diff --git a/ClaudeReport.md b/ClaudeReport.md index 27a2dd3e..ff189711 100644 --- a/ClaudeReport.md +++ b/ClaudeReport.md @@ -18,6 +18,7 @@ > to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`. ## Run-state (current) +- **R16 (2026-06-28) — full ClaudeQAPlan run STARTED.** Session-start: both emulators online (5554/5556), HEAD `8b7bbc2`, working tree carries a **dark-variant art batch** (pack_art_*_dark, together_empty/tonight_partner_prompt night variants → BRAND-DARK-COVERAGE progress) + my doc edits; baseline TBD. **Cheap gates (new step 3):** functions tests **24/24 ✅**; Android unit tests **found 5 failures → FIXED → 205 ✅** (**TEST-001**, test-vs-code drift: (a) `PartnerNotificationManagerTest` stubbed `isInQuietHours(any())` but the method's default `now: Calendar` param pinned a stale instant that never matched the call-time clock → stub now `any(), any()`; (b) `CloserBrandCopyTest` ≤64 cap predated the intentional 150-char flagship `primaryMessage` — `BrandMessageRotator` wraps it `maxLines=3` — → cap now applies to short slogans only, flagship bounded `1..160`. Both **test-side only**; production correct: quiet hours verified live R15, flagship is committed design `6d74c6a`). **theme-scan** 🔴9/🟠8/🟡32 (the 9 CRITICAL = C-THEME-001..009 already filed). **wiring-scan** 🔴0/🟠20/🟡35 (🔴0 = Pass N DoD met). **Done so far:** rebuilt+installed both; **smoke 5554 = 6/6 PASS, 0 blocked** (launcher + 5 notif cold-starts open & stay), 5556 in progress. **Theme triage — of the 9 filed C-THEME, 3 are NOT real shipped defects:** C-THEME-003 = `@Preview`-only (`WheelRevealPreview`) → false positive; theme-scan now **excludes @Preview** composables. C-THEME-006/007 = dead unused `PlaceholderScreen.kt` (replaced by the real dashboard; 0 source refs) → **file deleted**. **The other 6 are real → FIXED** (theme tokens): BucketList (badge→primaryContainer; AddItemDialog surface→surface + Cancel→secondaryContainer + Add→primary + CategoryChips→primary/surfaceVariant — also closes the Future.md "mixed dialog" note), DateMatch (heart→primaryContainer, count badge→error/onError), WheelHistory (lock→surfaceVariant/primary), QuestionThread (waiting banner→surfaceVariant). **theme-scan CRITICAL 9→0; build + unit tests green.** **R16 RESULT:** smoke **6/6 both (0 blocked)**; theme-scan **CRITICAL 9→0** (3 reclassified, 6 fixed); **C-THEME-001/002 + N-001 + N-002 verified LIVE (dark, 5554)**; units **205** + functions **24** green; dead `PlaceholderScreen.kt` deleted; theme-scan now excludes `@Preview`. **N-001/N-002 pruned.** Open now: **O-AGE-001** (P2 pre-ship) + 3 P3. **NEXT (R17):** live-confirm the 4 remaining C-THEME fixes (004/005/008/009) in both themes + the 6 in LIGHT on 5556; re-test **M-001** quiet-hours (backgrounded-push) to prune it; then resume passes A–N+P — esp. a live both-theme sweep of the new **dark-art batch** (BRAND-DARK-COVERAGE) + the D/E cornerstones. Uncommitted (user commits): unit-test fixes, 4 theme-fixed screens + BucketList/DateMatch, `scripts/theme-scan.sh`, **deleted** `PlaceholderScreen.kt`, dark-art batch, QA docs. - **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 A–J ClaudeQAPlan run (pure QA, no code changes) — FLAWLESS, 0 open P0–P3, 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 A–J — FLAWLESS (0 open P0–P3).** 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). **A–J:** 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`. @@ -46,36 +47,41 @@ | 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** | +| P1 | 0 | 0 | +| P2 | **2** (O-AGE-001 pre-ship, C-DARKART-002 decoupled dark-art) | **8** (6× C-THEME fixed, M-001 quiet hours, TEST-001 unit suite) | +| P3 | **3** (BRAND-DARK-COVERAGE, BRAND-ICON-CUSTOM, C-ORIENT-001) | 0 | -_R15: found + FIXED **3 bugs** — **M-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._ +_R16: ran the new **cheap gates** → found+fixed **TEST-001** (unit suite was silently red, 5 failures) → **205 unit + 24 +functions green**. **Entrypoint smoke 6/6 on BOTH emulators, 0 blocked.** Theme triage: of the 9 filed C-THEME, **3 were +not real shipped defects** (C-THEME-003 `@Preview` false positive [theme-scan now excludes @Preview]; C-THEME-006/007 dead +`PlaceholderScreen` [deleted]); **6 real → FIXED** → **theme-scan CRITICAL 9→0**, build+units green; **C-THEME-001/002 +verified LIVE (dark)**. Confirmed + **pruned** R15 fixes **N-001** (P1, live add/delete) + **N-002** (P2, Home "Date coming +up"). **M-001 carried** (not re-tested this round). Remaining open: **1 P2 pre-ship** (O-AGE-001) + **3 P3** (2 brand +backlogs + C-ORIENT-001). 4 C-THEME fixes (004/005/008/009) pending live confirm next round._ ## 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`. +> +> **R16 reclassified (NOT real shipped defects, removed from open):** **C-THEME-003** = `WheelCompleteScreen.kt:507` is inside `@Preview fun WheelRevealPreview()` — design-time only, never shipped → **false positive**; `scripts/theme-scan.sh` now **excludes `@Preview`** composables so it won't re-file. **C-THEME-006 / C-THEME-007** = `PlaceholderScreen.kt` (`SignalChip`/`PreviewPanel`) had **0 source references** (replaced by the real dashboard per `docs/qa/private-mvp-checklist.md`) → **dead code, file deleted**. | 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-functional** — `setCoupleId` was never called, so `coupleId` stayed `""` and `addItem`/`loadItems`/`toggleComplete`/`deleteItem` all silently `return`ed. Items could never be added, loaded, completed, or deleted. | `BucketListViewModel` resolves the couple itself in `init` via `CoupleRepository.getCoupleForUser()` (mirrors `MemoryLaneViewModel`), then `setCoupleId` → `loadItems`. | **Fixed — verified live R15** (add persists `enc:v1:`; complete sets flags; delete removes; list renders). Client-only, no deploy. Pending 1 confirm. | +| C-DARKART-002 | P2 | Visual / theme-variant art (Pass C/H) | **Dark-variant art does NOT render in the decoupled in-app-Dark + system-light state** (a common, supported config: user sets in-app **Dark** on a light/`auto`-system phone). Pack-art banners (`QuestionPackLibraryScreen.kt:223` via `packArtworkRes`) + ~7 literal `painterResource(R.drawable.illustration_/pack_art_)` sites (SpinWheel, AnswerReveal, Home, PlayHub, + debug ArtPreview) load via **raw `painterResource`**, which resolves the `-night` qualifier off the **system** uiMode, not the in-app theme — so the new `drawable-night-nodpi/` dark variants (BRAND-DARK-COVERAGE batch) only show when system=night. **Verified live R17 (5554):** in-app-Dark + system `auto` → **light pack art on dark** Question Packs; forcing system night=yes → correct dark aubergine art (variants themselves are correct + packaged). Recurrence of the C-DARKART-001 class for the direct-`painterResource` sites the R11 fix didn't cover. | Route these sites through `BrandIllustration` (resolves `-night` off `LocalAppInDarkTheme` via `createConfigurationContext` — the R11 pattern; also gains edge feathering), or drive a config override from the in-app theme. Re-run the decoupled-state check **both** directions after. | **Open** | +| C-THEME-001 | P2 | Dates / Bucket List | **AddItemDialog used a hardcoded light surface** (`Surface(color = Color.White)`, `BucketListScreen.kt`) → light dialog on dark. | → `MaterialTheme.colorScheme.surface`; also themed the whole dialog (Cancel→`secondaryContainer`, Add→`primary`, CategoryChips→`primary`/`surfaceVariant`) — closes the Future.md "mixed dark/light dialog" note. | **Fixed — verified LIVE R16 (dark): dialog surface dark, fields/chips/buttons readable.** Pending 1 confirm. | +| C-THEME-002 | P2 | Dates / Bucket List | **CategoryBadge + category filter chips used hardcoded light colors** (`Color(0xFFF3E8FF)`; ternary `Color(0xFFFFF8FC)` — the latter evaded the scanner's `color = Color(` regex). | → badge `primaryContainer`/`onPrimaryContainer`; chips `primary`/`surfaceVariant`. | **Fixed — verified LIVE R16 (dark): item-card "Adventure" badge + All/Adventure filter chips readable.** Pending 1 confirm. | +| C-THEME-004 | P2 | Questions / Discussion thread | **WaitingPhase "Your answer is saved" banner used `Color.White.copy(alpha=0.78f)`** (`QuestionThreadScreen.kt`). | → `surfaceVariant.copy(alpha=0.78f)` (children already use `onSurface`/`onSurfaceVariant`). | **Fixed R16** (theme-scan 0, build+units green) — pending live confirm. | +| C-THEME-005 | P2 | Wheel / History | **History premium-lock icon used `Color(0xFFF8F1FF)` bg + `Color(0xFFB98AF4)` tint** (`WheelHistoryScreen.kt`). | → bg `surfaceVariant`, tint `primary`. | **Fixed R16** (theme-scan 0, build+units green) — pending live confirm. | +| C-THEME-008 | P2 | Dates / Date Match | **"View matches" heart button used `Color(0xFFF3E8FF)` bg + `Color(0xFF56306F)` tint** (`DateMatchScreen.kt`). | → bg `primaryContainer`, tint `onPrimaryContainer`. | **Fixed R16** (theme-scan 0, build+units green) — pending live confirm. | +| C-THEME-009 | P2 | Dates / Date Match | **Match-count badge used `Color(0xFF8D2D35)` + `Color.White` text** (`DateMatchScreen.kt`). | → `error`/`onError` (semantic count badge, adapts both themes). | **Fixed R16** (theme-scan 0, build+units green) — pending live confirm. | +| O-AGE-001 | P2 | Release / store readiness (Pass O) | **No age gate / age verification despite adult-intimacy content.** Sign-up collects only email+password+confirm; Create Profile collects name+gender; `domain/model/User.kt` has **no DOB/age field**; the only "birthday" in-app is the *partner's* relationship special-date (`SpecialDatesSection`), not age. Yet the app ships sexual/intimacy content (Desire Sync). Google Play content-rating + sexual-content policy generally require an accurate maturity rating and may require an age gate. _(Static finding — 2026-06-28 QA-plan gap review; confirm against current Play policy + intended content rating.)_ | Add an 18+/age-appropriate gate where required + complete the Play content/maturity questionnaire to match actual content. **Pre-ship gate** (does not block per-round flawless). | **Open (pre-ship)** | +| C-ORIENT-001 | P3 | Visual / config (Pass C/O) | **App not portrait-locked; landscape layout unverified.** `AndroidManifest.xml` declares no `screenOrientation`, so `MainActivity` rotates to landscape — but no round has verified the landscape (or tablet/large-screen, minSdk 26 / targetSdk 35) layout renders correctly. _(Static finding — 2026-06-28 QA-plan gap review.)_ | Decide: **lock portrait** in the manifest if landscape isn't a supported experience, **or** certify the landscape layout (Pass C orientation check). | **Open** | | M-001 | P2 | Settings / notifications | **Quiet hours did not suppress backgrounded/killed partner pushes.** "Quiet hours — 10 PM–8 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. | +| TEST-001 | P2 | QA infra / unit tests | **Unit suite was silently RED (5 failures) — the regression net was non-functional, undetected until R16 ran it for the first time** (test-vs-code drift). (a) `PartnerNotificationManagerTest` (4×) stubbed `quietHoursManager.isInQuietHours(any())`, but the method's default `now: Calendar = Calendar.getInstance()` param made the stub pin the instant captured at stub time → never matched the SUT's call-time clock → `MockKException`. (b) `CloserBrandCopyTest` asserted every privacy message `≤64` chars, which predated the **intentional** 150-char flagship `primaryMessage` (commit `6d74c6a`; `BrandMessageRotator` wraps it at `maxLines=3`). | **Test-side only** (production correct: quiet hours verified live R15; flagship is committed design). Stub → `isInQuietHours(any(), any())`; brand test caps short slogans `≤64` + flagship `1..160`. | **Fixed — verified R16** (`./gradlew testDebugUnitTest` 205 ✅, functions 24 ✅). 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 A–J 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**.) +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** · **N-001** · **N-002** — all fixed and re-verified (R16 pruned **N-001** [Bucket List non-functional → CRUD works; confirmed live add/delete] + **N-002** [Date Builder no-op → Home "Date coming up"; confirmed live]) (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 A–J 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. diff --git a/Future.md b/Future.md index df2e7290..0a6c08fe 100644 --- a/Future.md +++ b/Future.md @@ -20,6 +20,13 @@ Improvement & feature ideas surfaced while QA-testing as a consumer (each works light and dark, pixel-diffs them, and fails on unexpected white backgrounds or invisible text. When done, run it in CI against every UI PR. +- **Instrumented / on-device test coverage (currently 0 androidTest).** `app/src/test` has 19 solid unit tests + (encryption, rate limiter, quiet hours, streak, entitlement, …) but `app/src/androidTest` is empty — there is no + automated on-device net for UI behavior, navigation, or DB/DataStore integration; the live QA passes + scanners are the + only thing catching that class. Add a small **Compose UI / Espresso smoke** (sign-in → pair → answer daily Q → open a + game → send a message) wired into the per-round gate (alongside `qa/entrypoint_smoke.sh`), then grow it. *Prompted by:* + the R-? QA-plan gap review — the playbook now runs `./gradlew testDebugUnitTest` but has no instrumented suite to run. + - **✅ DONE — Consistent brand glyphs across game cards + waiting surfaces.** G-set + G2 (17 glyphs) in `res/drawable-nodpi/glyph_*.xml`; **13 wired + verified live:** every Play-hub card (This or That, How Well, Desire Sync, Connection Challenges, Memory Lane, Date Match, Plan Date, Question Packs, Bucket List, Past Games — Spin the @@ -50,6 +57,33 @@ Improvement & feature ideas surfaced while QA-testing as a consumer (each works > Artwork to generate (ChatGPT prompts, house-style-matched) lives in `ClaudeBrandingReview.md`, not here. +## Roadmap — features & strategy (consolidated from the old `FUTURE.md`, 2026-06-28) + +**Unbuilt games (Play Hub is live; these remain):** +- **Would You Rather / Truth or Dare** — tiered sweet→spicy, consent-gated; reuses the deck + match engine. Strong **premium "spicy" tier** lever. +- **Daily Sync / Rose-Bud-Thorn** — one-tap emotional check-in, see partner's; small/new, free retention driver. + +**"2026, not 2019" differentiators (strategic):** +- **AI-personalized prompts** — server-side (latest Claude) generate/weight prompts from the couple's history/season/unexplored topics; add a `generatePrompts` callable (Cloud Functions already in place). Biggest modern lever; pairs with content-metadata routing below. +- **Async + real-time hybrid** — partner-presence ("they're here now") for live co-play, everything still async-friendly (long-distance). +- **Multi-modal answers** — voice/photo answers (esp. Memory Lane + daily check-ins). +- **Home/lock-screen widgets / live updates** — glanceable today's-prompt + partner status (Glance/Wear surface). +- **Gentle gamification** — forgiving "gentle streaks" (grace days) + shared wins, not loss-aversion/guilt. +- **Consent-first spicy content** — Intimacy/Dare tiers opt-in, double-confirmed, reveal-only-on-overlap. + +**Cross-platform:** Android-first is fine for MVP; iOS is the strategic gap (couples split devices). Decision + plan live in `ClaudeiOSPlan.md` — don't start before release/trust polish. + +## Product polish (consolidated from the old `FUTURE.md`) +- **Skeleton/loading states over bare spinners (P8).** `LoadingState` exists but many screens still use a bare `CircularProgressIndicator`. Add skeletons for question lists, game histories, home modules, paywall offerings, sync/reveal waits. Acceptance: no primary route shows an isolated spinner on an otherwise blank screen. +- **Paywall / store value framing (P12).** Paywall has benefits/restore/legal/RevenueCat; needs stronger value framing, real offering/trial clarity, screenshots/previews, and test coverage before release. (Overlaps Pass K/O.) +- **Content metadata & personalization (P13).** The bank is clean; the next leap is *routing*, not more questions — tag mood/depth/relationship-stage/conflict-safe/intimacy-level/time-needed and extend the selection APIs so prompts adapt to skipped topics, relationship length, and recent answers. + +## Release / pre-ship (consolidated + re-verified 2026-06-28) +- **Real release config before any store submission.** Confirmed still open: `app/build.gradle.kts` `versionCode = 1` / `versionName = "0.1.0"` and `core/navigation/ExternalLinks.kt` legal URLs are placeholder TODOs (`https://closer.app/privacy|terms|subscription-terms`). _Already done:_ a release-blocking Gradle check now fails the build if `RC_API_KEY` is unset/placeholder (`build.gradle.kts:106–110`), so the old "add a gradle guard" sub-item is closed. Remaining: set real version, real legal/support URLs, real RC key + verify offerings/purchase/restore on internal testing. (Tracked by **Pass O** release-readiness.) + +## Help & support surface (consolidated — old "Notes to Consider") +A future Help/Support screen could include: contact support · report a bug · send feedback · FAQ · subscription/billing help · pairing help · recovery-phrase/account help · app version + build number · optional "copy diagnostics" button. +