docs: consolidate Future backlog, update ClaudeQAPlan/ClaudeReport, note FUTURE.md removal in Engineering Manual

This commit is contained in:
null 2026-06-28 12:45:54 -05:00
parent 7a9b9eaa9d
commit faa0d9007f
4 changed files with 185 additions and 26 deletions

View File

@ -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 emulators; **installed build == HEAD** (rebuild+install if unsure — never QA a stale APK); baseline clean
(both free, 0 active sessions, logcat 0 FATAL). (both free, 0 active sessions, logcat 0 FATAL).
2. **Discovery ritual:** reconcile routes/notifications/features/assets/backend with coverage; fold new surfaces in. 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` 3. **Run the cheap gates FIRST (before live driving):** (a) the **automated test suites**`./gradlew testDebugUnitTest`
(Pass C), `scripts/wiring-scan.sh` (Pass N). File 🔴/🟠 to `ClaudeReport.md`; record counts in coverage. + `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 **AN + P** (K money-path + 4. **Run the passes report-only**, sub-batched to one context window each — recurring set **AN + P** (K money-path +
O release gates only when a sandbox device / pre-ship is in scope). Checkpoint the MD files after each chunk. 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** + 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 / 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 theme / manifest / nav / notifications**. `FAIL` = an app crash (real bug); `BLOCK` = push not delivered (flaky
emulator FCM — rerun, not a bug). 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: - **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-<date>.md` before looking at any screen. - **Pass C:** run `scripts/theme-scan.sh` and review `/tmp/claude-theme-scan-<date>.md` before looking at any screen.
- **Pass N (+ discovery ritual):** run `scripts/wiring-scan.sh` and review `/tmp/claude-wiring-scan-<date>.md` before - **Pass N (+ discovery ritual):** run `scripts/wiring-scan.sh` and review `/tmp/claude-wiring-scan-<date>.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 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) 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 **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 + Treat cosmetic/branding/theme/manifest/splash commits as **capable of deep crashes** — re-run the cold-start +
notification smoke after them. 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 - **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 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**.) **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 - **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 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 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 (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 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. 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; - **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 admin-only writes rejected from clients; service-account JSONs never committed; no plaintext/secrets in logcat; temp
files deleted. 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 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 disabled notifications should fail gracefully with in-app state still correct, never with lost data or broken routing
after permission is restored. 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 - **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 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/ 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 - **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 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. 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 - **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) 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 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 - **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** **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. (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 - **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. 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 - **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. 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 - **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). 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 - **Touch targets:** interactive targets ≥ **48dp** (icon buttons, chips, nav, close/back, reaction buttons, swipe-deck
actions). Flag anything smaller. actions). Flag anything smaller.
- **Keyboard / external input:** with a hardware keyboard, forms (sign-up, message, capsule, profile) tab in a sane - **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, - **Delete account** → confirmation → account + couple data cascade (`onUserDelete`), partner unpaired + notified,
re-create with the same email is a clean slate (overlaps G). 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 - **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). - Every toggle survives **process death + reinstall-with-data** (overlaps F).
### Pass N — Daily question, reveal, check-ins & the other interactive features ### 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 - **Deep links / Android App Links:** `closer://` **and** any `https` App Links (`assetlinks.json`) open the correct
screen with auth/membership re-checked (overlaps E). screen with auth/membership re-checked (overlaps E).
- **Permissions & manifest:** the manifest declares only what's used; runtime prompts (POST_NOTIFICATIONS, camera, mic, - **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 - **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 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. (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 - **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** live; version code/name bumped; store listing/screenshots are the brand pass (H); min/target-SDK **device matrix**
(Methodology) covered. (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) ### 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, **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 (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 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. 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 **24 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 - **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** 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. 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) ## Fix phase (only AFTER all passes of the round complete)
- Work strictly by severity: **all P0 → P1 → P2 → P3**. - 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 <file>` 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 - **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 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 (the **user** commits per issue/cluster — never run git yourself; see Guardrails). Don't start the next until the
current is verified. current is verified.
- **Real-path verification gate (do NOT mark Fixed without it):** verify the fix through the **same path the user hits**, - **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 (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/ 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 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 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 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. "flawless" but **must be GREEN before any store submission**. Don't re-open a clean pass within the same round.

View File

@ -18,6 +18,7 @@
> to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`. > to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`.
## Run-state (current) ## 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 AN+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. - **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). - **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`. - **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`.
@ -46,36 +47,41 @@
| Severity | Open | Fixed (pending 1 confirm) | | Severity | Open | Fixed (pending 1 confirm) |
|---|---|---| |---|---|---|
| P0 | 0 | 0 | | P0 | 0 | 0 |
| P1 | 0 | **1** (N-001 Bucket List) | | P1 | 0 | 0 |
| P2 | **9** (C-THEME-001..009) | **2** (M-001 quiet hours, N-002 Date Builder) | | 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 | **2** (BRAND-DARK-COVERAGE, BRAND-ICON-CUSTOM) | **0** | | 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 _R16: ran the new **cheap gates** → found+fixed **TEST-001** (unit suite was silently red, 5 failures) → **205 unit + 24
Date Builder "Create Plan" no-op) — all verified live, pending 1 confirm. **9 new open P2 theme defects** surfaced by functions green**. **Entrypoint smoke 6/6 on BOTH emulators, 0 blocked.** Theme triage: of the 9 filed C-THEME, **3 were
`scripts/theme-scan.sh` (C-THEME-001..009). 2 P3 brand-asset backlogs remain open._ 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) ## 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`. > 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 | | 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-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 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-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 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-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-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 "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-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 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-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-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-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-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. |
| 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** | | 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-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-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** |
| 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. |
| 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. | | 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. |
| 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-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)** | | 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) ## 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**.) 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 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) ## 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. - **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.

View File

@ -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, 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. 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 - **✅ 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 `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 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. > 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:106110`), 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.
<!-- <!--
Completed (2026-06-27, Future.md backend pass — deployed + verified live): Completed (2026-06-27, Future.md backend pass — deployed + verified live):
- subscription_entitlement_changed push — new Cloud Function `onEntitlementChanged` (users/{uid}/entitlements/premium - subscription_entitlement_changed push — new Cloud Function `onEntitlementChanged` (users/{uid}/entitlements/premium

View File

@ -1117,7 +1117,10 @@ SCRIPTS.md
.kotlin/ .kotlin/
``` ```
> **Note (2026-06): the gitignore list uses `FUTURE.md` (uppercase) but the tracked file is `Future.md` (mixed case).** Linux filesystems are case-sensitive so these are different paths; the gitignore does not actually block `Future.md`. The ClaudeQA docs (`ClaudeQAPlan.md`, `ClaudeReport.md`, `ClaudeQACoverage.md`, `ClaudeBrandingReview.md`, `ClaudeiOSPlan.md`) and `Future.md` are explicitly tracked. If you create new top-level docs and want them gitignored, either match the existing case in `.gitignore` (`Future.md`) or pick a case and use it consistently. Do not block `Future.md` and then create `future.md` thinking you're safe. > **Note (2026-06): the gitignore list uses `FUTURE.md` (uppercase) but the tracked file is `Future.md` (mixed case).** Linux filesystems are case-sensitive so these are different paths; the gitignore does not actually block `Future.md`. The ClaudeQA docs (`ClaudeQAPlan.md`, `ClaudeReport.md`, `ClaudeQACoverage.md`, `ClaudeBrandingReview.md`, `ClaudeiOSPlan.md`) and `Future.md` are explicitly tracked. If you create new top-level docs and want them gitignored, either match the existing case in `.gitignore` (`Future.md`) or pick a case and use it consistently. Do not block `Future.md` and then create `future.md` thinking you're safe. **(2026-06-28: the legacy uppercase
`FUTURE.md` — a stale, untracked duplicate backlog — was consolidated into the tracked `Future.md` and removed. The
`.gitignore` entry is kept so any accidentally-recreated uppercase copy stays ignored, but the one canonical backlog is
`Future.md`.)**
### Versioning ### Versioning