Closer/ClaudeQAPlan.md

865 lines
88 KiB
Markdown
Raw Normal View History

# Claude QA Playbook — Full-App QA → Fix → Re-QA until flawless
> Reusable QA plan for the Closer app. Run report-only first, fix everything, then re-QA until a clean round.
> Progress/state is tracked in **ClaudeReport.md** (issues) + **ClaudeQACoverage.md** (coverage matrix), which are
> the authoritative source of truth. See the Continuity section before resuming.
>
> **Program roadmap:** **Part 1** = Android QA (this doc) → **Part 2** = build the iOS app to Android's current
> parity → **Part 3** = run these same passes on iOS + a cross-platform (Android↔iOS) pass. **Parts 2 & 3 live in
> `ClaudeiOSPlan.md`** (note: iOS build/run/QA requires macOS — not possible from this Linux box).
## 📖 Architecture reference (read BEFORE testing the matching area)
For each Pass below, before you start, read the relevant section of [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) — it documents the architecture, the wire-format contracts, the security invariants, and the [Known landmines](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes) (bugs that cost real debugging time and are easy to re-introduce).
**This is bidirectional — the manual is a LIVING document, not a read-only reference.** Read it before; **write back to it after.** Whenever a round fixes a bug, changes a contract/flow/gate, or finds the manual stale or missing something, update the manual in the same chunk (see *Where every finding goes*, the *Docs update rule*, and the *MANDATORY retrospective* — all now route durable engineering truth here). Treat it as part of every fix, same as `ClaudeReport.md`/`ClaudeQACoverage.md`.
| Pass | Manual section to read first |
|---|---|
| A — Couple-shared premium | [Premium-gated features and gate pattern](docs/Engineering_Reference_Manual.md#premium-gated-features-and-gate-pattern) · [Billing](docs/Engineering_Reference_Manual.md#billing) |
| B — Games lifecycle | [Game session push semantics (idempotent flag-claim)](docs/Engineering_Reference_Manual.md#game-session-push-semantics-idempotent-flag-claim) · [Foreground game-alert banner](docs/Engineering_Reference_Manual.md#foreground-game-alert-banner-r10) · [F-RACE-001](docs/Engineering_Reference_Manual.md#f-race-001-duplicate-game-start-push-on-rapid-partner-update) |
| C — Visual (light+dark) | [Daily question lifecycle](docs/Engineering_Reference_Manual.md#daily-question-lifecycle) · [C-NAV-001](docs/Engineering_Reference_Manual.md#c-nav-001-back-from-home-resurfaces-onboarding-auth) · [Back-stack gotchas](docs/Engineering_Reference_Manual.md#back-stack-gotchas-c-nav-002-c-nav-003) · [C-HOME-001](docs/Engineering_Reference_Manual.md#home-duplicate-pending-action-card-c-home-001) |
| D — Security & encryption | [End-to-end encryption model](docs/Engineering_Reference_Manual.md#end-to-end-encryption-model) · [Firestore security rules](docs/Engineering_Reference_Manual.md#firestore-security-rules) · [Encryption versions](docs/Engineering_Reference_Manual.md#encryption-versions) |
| E — Notifications | [Notifications](docs/Engineering_Reference_Manual.md#notifications) · [Notification deep-link routing](docs/Engineering_Reference_Manual.md#notification-deep-link-routing) · [E-GAME-001](docs/Engineering_Reference_Manual.md#e-game-001-notification-deep-link-landed-in-stale-finished-game) · [E-GAME-002](docs/Engineering_Reference_Manual.md#e-game-002-game-start-push-easy-to-miss-when-app-is-foreground) |
| F — Resilience | [End-to-end encryption model](docs/Engineering_Reference_Manual.md#end-to-end-encryption-model) · [Known limitation: single-device keys](docs/Engineering_Reference_Manual.md#known-limitation-single-device-keys) |
| G — Account creation / fake-account | [Authentication and pairing flow](docs/Engineering_Reference_Manual.md#authentication-and-pairing-flow) · [Rate limiting on accept](docs/Engineering_Reference_Manual.md#rate-limiting-on-accept) |
| H — Branding & artwork | `ClaudeBrandingReview.md` (this repo) · `docs/brand/visual-identity.md` |
| I — Performance | [Engineering conventions](docs/Engineering_Reference_Manual.md#engineering-conventions) · [Where to look first](docs/Engineering_Reference_Manual.md#where-to-look-first) |
| J — Accessibility | [CloserTheme](docs/Engineering_Reference_Manual.md#ios-specific-notes) · [Engineering conventions](docs/Engineering_Reference_Manual.md#engineering-conventions) |
**If you find a bug that LOOKS like it might be a re-introduction of a known landmine** (above table or [Known landmines](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes)), stop and verify the fix is still in place before filing a new ID — it may be a regression on a known issue, not a new bug.
## Where every finding goes (route it here — exactly one home each)
| What you found | Where it goes | Form |
|---|---|---|
| **A bug** — broken / incorrect / crashing / insecure, premium bypass, wrong-or-missing notification, dead-end nav | **`ClaudeReport.md`** | Table row: stable ID (`A-001`, `E-003`…) + severity (P0P3) + repro + status |
| **An idea / improvement** — works but could be better, confusing copy, missing affordance, rough-but-not-broken flow, "it'd be great if…", feature idea | **`Future.md`** `## QA` | Short title + what prompted it + suggested improvement |
| **New artwork to create** — illustrations, glyphs, image-gen prompts | **`ClaudeBrandingReview.md`** | House-style prompt + placement |
| **What got tested + its status** (pass / fail / todo / deferred) | **`ClaudeQACoverage.md`** | Coverage cell (the resume anchor) |
| **Durable engineering knowledge** — a fixed bug's root cause + how it's easy to re-introduce, a new architecture fact / data path / wire-format contract / security invariant / gate pattern, or anything the manual is now stale/missing about | **[`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md)** (esp. [Known landmines and recent fixes](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes)) | New landmine entry (ID + cause + the guard) and/or an updated architecture/gate/flow section |
- A branding **defect** (mis-colored, clipped, off-brand, low-contrast art) is a **bug → `ClaudeReport.md`**, not a brand
idea — only *new art to create* goes to `ClaudeBrandingReview.md`.
- **ONE canonical home per fact; everywhere else is a pointer (ID/anchor), never a paraphrase.** This is the rule that
keeps the five docs from duplicating each other (and wasting tokens re-stating the same lesson). Route by *purpose*:
the **defect** (repro/severity/status) → `ClaudeReport.md` (transient — prunes to an ID after one confirm); the
**substance** (root cause / why it's fragile / how to not re-introduce it) → the **Engineering Reference Manual**
(permanent, engineer-facing); the **reflex** (how to FIND the class next round) → this `ClaudeQAPlan.md` Pass
(generalized, citing the ID); **coverage status**`ClaudeQACoverage.md`; **cross-session ops not in the repo**
(accounts, tooling, auth) → `memory/`. State a fact in its home once; elsewhere cite the ID. Don't restate a fix in
four docs.
- **The Engineering Reference Manual is a LIVING document — read it before a pass, write back to it after.** When a
round teaches the codebase something durable (a fixed bug's re-introduction risk, a new/changed architecture fact,
data path, contract, gate, flow, collection/Function/route, or the manual disagreeing with reality), update the manual
in the **same chunk**. **A fix is not complete until its durable substance is in the manual** (see the
MANDATORY-retrospective rule). The report row and the Pass reflex just reference the manual's landmine ID — they don't
re-tell it.
- Logging an idea in `Future.md` is **never** a substitute for filing a real defect: if it's broken, it gets an ID in
`ClaudeReport.md` too.
- Bug lifecycle: filed in `ClaudeReport.md` → fixed → kept **one** confirmation round → pruned to the archived-ID line
(detail lives in git). `Future.md` ideas sit in the backlog until built. (See **Report hygiene** under Reporting.)
## Context
Drive the real app on both emulators, verify each thing live, report, fix, re-verify. Five QA dimensions:
1. **Couple-shared premium** — if EITHER partner is premium, **all** premium features unlock for **both**.
2. **Games** — each starts, plays, **joins, resumes**, finishes, **and reopens results** correctly on both devices.
3. **Full visual pass, light + dark** — every screen, text readable, nothing clipped/invisible.
4. **Security & encryption (cornerstone)** — every private field is ciphertext at rest, rules hold against
non-members, keys/recovery are sound. Findings here default to P0.
5. **Notifications** — the **full suite**: every type delivers to the right partner (foreground/background/killed),
deep-links correctly, opens the right destination on **both clients**, covers all **game/join-game** flows, handles
stale notifications, and leaks no private content.
Scope decisions: **exhaustive** visual pass (all ~50 screens, both modes); **full scope incl. pre-pairing** flows
(fresh throwaway account); **couple-shared everywhere** — per-user gates are bugs, fixed by routing through
`core/billing/CouplePremiumChecker.kt`; **full notification suite** — every type, game + join-game pushes, deep-links,
stale-notification handling, and all in-app paths into joining/resuming/results, verified on **both clients**.
**Early known signal:** only chat uses `CouplePremiumChecker`; games/packs/dates/wheel gate on the user's own
`EntitlementChecker.isPremium()` — so premium almost certainly does NOT unlock for the free partner there. Pass A
confirms + enumerates this; the fix phase applies couple-shared everywhere.
## Execution mode — run to completion (autonomous; do NOT stop)
- **Do not stop to check in or ask for approval.** Run all passes (AJ) → the fix phase → re-QA rounds **continuously
until a flawless round** (zero open P0P2, Passes D + E clean, every game fully played through, all notification
routes verified, navigation/back-stack verified). Don't hand control back early.
- **Unblock yourself:** if anything **blocks progress** (a stale/blocking session, a crash, a build break, a missing
prerequisite state, a broken nav path that prevents reaching a screen), **fix it immediately and continue** — even
though passes are otherwise report-only. Blocking issues are fixed inline so the run can proceed; non-blocking
findings are still logged and fixed in the fix phase.
- **"Once executed, complete it":** never declare done before the Definition of Done is met — keep cycling fix → re-QA
until flawless, then stop.
- **Context limits ≠ stopping — do NOT hand back to the user when context fills.** The harness auto-summarizes a long
conversation and continues in the next window; you continue **without the user**. (You cannot self-invoke `/compact`
— and you don't need to; auto-compaction handles it.) The **committed `ClaudeReport.md` run-state + `ClaudeQACoverage.md`
are the authoritative state** and survive any compaction — after a summary, **re-read them and continue at the next
chunk**. Never pause a run merely because context is getting long; only stop for a true blocker (a denied gated action
even with standing auth, or the macOS requirement for iOS).
- **Commit before anything interruptible** so a mid-chunk compaction never loses progress. Keep chunks atomic; if a
chunk is cut off mid-way (e.g., a game session left active), the **session-start ritual recovers it** (clear the stuck
session via in-app "End their game", then redo that chunk). Right-sized chunks (see Batch sizing) make this rare.
- **Don't pause for "by-design vs bug":** log the ambiguous finding and keep going (don't unilaterally rewrite
deliberate design — the log captures it). Never halt the run to ask.
- **Only true stop = a gated action you cannot perform.** Production deploys, admin Firestore writes/seeds, and
entitlement toggles still need per-occurrence authorization (the classifier enforces this regardless of this doc).
If one is genuinely required to proceed and is denied, do **all** other work first, then surface only that single
blocker — don't halt the whole run for it.
## Methodology (every pass)
- **EVIDENCE OVER ASSUMPTION — read the logs, never assume, always verify (the #1 rule).** Every conclusion —
`pass`, `fail`, `fixed`, "it works", "the notification didn't open" — must be backed by **observed evidence**, never
by what the UI *appears* to do or by reasoning about the code. Concretely:
- **Read `logcat` on EVERY action, not only when something looks wrong.** `logcat -c` before a tap/flow, then after,
scan for `FATAL EXCEPTION`/ANR/`PERMISSION_DENIED`/exceptions. **Absence of a visible symptom ≠ success** — a screen
that "looks fine" can be masking a swallowed exception, a denied read, or a crash on another device.
- **Verify with ground truth, not appearance:** confirm persisted state via **admin reads** (Firestore), confirm
delivery via `notification_queue`/`dumpsys notification`, confirm routing via the landed screen + back stack,
confirm encryption via the raw stored bytes. "Looked right" is not verified.
- **Don't theorize a root cause — reproduce it and read the stack.** If behavior is "didn't work / closed / flashed",
pull the crash log FIRST (this session's bug was misdiagnosed by reasoning until the live stack named the splash NPE).
- **Don't trust a synthetic pass** (`am start`, admin write, direct call) for launch/notification/permission paths —
verify through the **real** channel (see Reproduction fidelity). A green that didn't exercise the user's path is not green.
- Devices: **5554 (QA)**, **5556 (Sam)**, paired; one **fresh throwaway account** for pre-pairing flows.
- Drive via adb tap/swipe; resolve coords from `uiautomator dump` bounds; downscale screenshots to read;
scan `logcat` for `FATAL EXCEPTION`/ANR on each screen.
- Premium toggled via `scratchpad/set_premium.js` (admin, **user-authorized each time**).
- Theme toggled via **Settings → Appearance (Light/Dark)** (`MainActivity` `ThemeMode`).
- **REPORT-ONLY during passes — never fix mid-pass.**
- **THINK AS A CONSUMER — approach everything from different angles.** Beyond "does it work", constantly ask *"is this
what a real person would expect / want here? is this delightful, confusing, or annoying?"* Come at each flow from
multiple angles (first-time user, returning user, the partner who didn't start it, someone tapping fast, someone
reading carefully, the skeptic, the impatient). Vary inputs, depths, orders, and entry points (don't repeat one
happy path). A thing can be bug-free yet still *worse than it should be* — notice that too.
- **CAPTURE IMPROVEMENT / FEATURE IDEAS → `Future.md` (section `## QA`).** Bugs (broken/incorrect behavior) go to
`ClaudeReport.md` as always. But anything that *works yet could be better* — confusing copy, a missing affordance,
a rough-but-not-broken flow, a "it'd be great if…" feature idea — append it to **`Future.md` under `## QA`** with a
short title, what prompted it, and the suggested improvement. This is an idea backlog, **not** the bug log; logging
here is never a substitute for filing an actual defect in `ClaudeReport.md`.
- **Environment (senior-QA rec):** prefer the **Firebase Local Emulator Suite or a dedicated staging project** over
production — isolates test data, makes seeding / entitlement toggles / D3 negative tests **free** (no gated prod
writes), and avoids polluting real users. Caveat: App Check, RevenueCat IAP, and real FCM/APNs push need real
services — run those against staging/prod with test accounts. (We've been on prod with test accounts — works, but
every seed/toggle/deploy hits the gate.)
- **Device/OS matrix:** don't certify on one emulator only — cover **minSdk + targetSdk**, a **small** and a **large**
screen, and at least one **physical device** (App Check / Play Integrity behave differently on emulators).
- **Automate the regression smoke:** capture the smoke checklist as a runnable script (adb/Maestro) so every round
re-checks it cheaply instead of by hand. **Built:** `qa/entrypoint_smoke.sh <serial> <recipient_uid>` (+ helper
`qa/qa_push.js`) — the cold-start / entry-point launch-integrity smoke. It launches via the launcher AND sends a
**real** push to a killed (`am kill`) app and **taps the actual OS notification** for each type, asserting the app
**opens and STAYS** (process alive, 0 FATAL, off the launcher). This is the smoke that catches the "opens-and-closes"
splash-crash class that `am start` can't. Run it **every round and after any commit touching MainActivity / splash /
theme / manifest / nav / notifications**. `FAIL` = an app crash (real bug); `BLOCK` = push not delivered (flaky
emulator FCM — rerun, not a bug).
- **Test-data hygiene:** keep known test accounts; clean up artifacts (stray messages/reactions/sessions) between
rounds so they don't masquerade as bugs.
- **Evidence standard:** every filed bug must be reproducible from text alone: build/commit, device, account, theme,
app/process state, screen/route, exact tap/input sequence, expected result, actual result, and whether logcat showed
a crash/ANR/permission denial. Screenshots/videos are helpful but never the only evidence because session artifacts
may not survive compaction.
- **Flake policy:** if something fails once and then passes, do not dismiss it. Repeat from a clean state, vary timing
(rapid tap / slow network / background-resume), inspect logs, and file it as intermittent if it cannot be made fully
deterministic. Intermittent routing, notification, encryption, duplicate-write, or crash behavior is still a bug.
- **Reproduction fidelity (how we catch DEEP bugs) — the test harness must exercise the SAME path as the user.** A
synthetic shortcut (`am start` extras, admin writes, calling a function directly, `am force-stop`) can **pass while the
real path crashes** — the splash-handover NPE only fires on a real notification cold-start, and `am force-stop` can't
even receive FCM. So for launch / notification / permission / IPC / deep-link behavior, reproduce through the **real OS
mechanism** (real push tapped from the shade, real launcher cold-start, real permission dialog). Record **which angle**
proved it in `ClaudeQACoverage.md`; "synthetic/UI-shortcut only" is **not** a pass for these paths.
- **Symptom→inspection reflexes (apply before theorizing a root cause):** (1) "opens-and-closes / flashes / silently
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)
Treat cosmetic/branding/theme/manifest/splash commits as **capable of deep crashes** — re-run the cold-start +
notification smoke after them.
## Living discovery ritual (before each round, and whenever reality disagrees with the docs)
The app is allowed to grow; the QA plan must keep up. Before a pass or chunk, quickly inventory the current code/app
surface and reconcile it with `ClaudeQACoverage.md`:
- **Routes/screens:** inspect `core/navigation/AppRoute.kt`, navigation graph call sites, Settings sub-pages, dialogs,
bottom tabs, deep links, and any new composables reachable by buttons/cards.
- **Notifications:** inspect notification type enums/classes, Cloud Function triggers, Android intent/deep-link handling,
notification channels/actions, FCM token registration, and Android runtime notification permission paths.
- **Features/gates:** grep for premium checks, permission requests, media pickers, billing/paywall entry points,
destructive actions, account/couple lifecycle actions, and admin/server-only writes.
- **Assets/content:** inventory new drawables, `drawable-night*` variants, pack art, empty states, strings, feature flags,
remote config, and any debug-only screens that should not ship.
- **Backend/rules:** inspect Firestore rules, indexes/queries, Functions triggers/callables, Storage paths, scheduled
jobs, and migrations for new data shapes or access paths.
- **Docs update rule:** if the inventory finds a page, feature, notification, asset, state, backend path, or edge case
missing from the playbook/coverage, update `ClaudeQAPlan.md` and `ClaudeQACoverage.md` before marking the chunk done.
If it is product polish, also add it to `Future.md`; if it needs new artwork, add it to `ClaudeBrandingReview.md`.
**And if the discovery is a durable engineering fact (new route/collection/Function/flag/contract, a changed wire
format, a renamed file, a gate/flow that the manual describes wrongly or omits), update
[`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) in the same chunk** — the discovery
ritual is exactly when the manual drifts out of date, so reconcile it then, not "later".
## Multi-angle attack mandate (go DEEPER than "does the happy path work")
A capability can pass via the UI yet fail when hit directly. Probe each meaningful capability (read/write a private
field, gate a premium feature, deliver/route a notification, start/finish a game, pair/unpair, create an account)
from as many **independent angles** as apply — not just the in-app happy path:
- **Real UI** (play-as-user) — the baseline angle.
- **Crafted intent / deep-link** — fire the exact intent a notification/link carries (bypasses UI nav) to test routing
in isolation; also send **malformed/missing extras** → must route gracefully or no-op, never crash.
- **Raw API against the DEPLOYED backend** — hit Firestore/Storage/Functions REST **directly** with a real token,
as a **member AND a non-member**, to exercise rules + App Check from OUTSIDE the app. A non-member (or no-App-Check)
request must be **DENIED** — App Check `403` or rules `PERMISSION_DENIED`. The member request characterizes which
layer enforces. **Any unauthorized `200` returning couple data = P0.**
- **Admin inspection (ground truth)** — read the RAW stored docs/objects (admin bypasses rules) to assert what is
actually persisted: ciphertext only, no plaintext, no raw keys/invite-seeds, no private content in pushes.
- **Concurrency / race** — two partners (or two rapid taps) hit the same thing at once.
- **Killed / cold state** — kill with **`am kill <pkg>`**, NOT `am force-stop`: a force-stopped app is in Android's
*stopped* state and is **excluded from FCM broadcasts** (`GCM broadcast …result=CANCELLED`), so the push never
arrives and you get a false "no notification". Then deliver a **real** push and **tap the actual OS notification**
(one at a time — clear the shade first; tapping a *grouped summary* launches with no extras and falsely lands on
Home). `am start … --es type …` is **not** equivalent to a real notification tap (different launch path — see the
crash-triage note in Pass E). Also cold-start straight onto a deep link.
- **Malformed / abusive input** — oversized, empty, rapid-fire, injection-ish, forged FCM payloads, replayed/expired
tokens & invite codes.
- **Offline / flaky** — drop network mid-action → graceful failure, recover on reconnect.
Record **which angles** were tried per area in `ClaudeQACoverage.md`. For security- or data-sensitive capabilities,
"UI happy path only" is **not** a `pass`. **D3/Pass G negative access MUST be executed live via the raw-API angle each
round — never deferred to "only 2 emulators."** (Mint a token for a non-member UID via admin → exchange for an ID
token via the Identity Toolkit REST `signInWithCustomToken` → use it as Bearer against the Firestore REST API.)
## Continuity & resumability (this effort WILL span many context windows — don't lose state)
State lives in **files**, not memory:
- **`ClaudeReport.md`** = the issue log (committed). Each issue row is **self-contained in text** (repro + expected
+ actual) — screenshots are session-only and won't survive a compaction; never rely on a screenshot path alone.
- **`ClaudeQACoverage.md`** = the coverage matrix: every screen×mode, feature×premium-state, game×lifecycle,
notification×{foreground,background,killed}, each `todo | pass | fail→id | not implemented→Future.md | blocked→id`.
The resume anchor.
- **`Future.md`** (`## QA`) = the non-bug improvement/idea backlog; **`ClaudeBrandingReview.md`** = the branding/artwork
review + image-prompt backlog. Both committed alongside the report/coverage.
- **Persistent memory** (`memory/`): QA methodology + exact commands; emulator↔account↔coupleId mapping;
`scratchpad/set_premium.js` + admin tooling; the couple-shared-premium-everywhere goal + the per-user-gate gap.
- **Run-state header** pinned at the TOP of `ClaudeReport.md`, always current: `Round N | Pass X | Chunk Y |
NEXT ACTION: …` — first thing to read, last thing to update before stopping.
- **Stable issue IDs**: `A-001 / B-002 / C-… / D-… / E-…` (pass-letter + number); coverage references the ID for
every `fail`. Never renumber or reuse.
- **Source of truth**: the two MD files are authoritative; the TodoWrite list is scratch for the current chunk only.
Update the MD files + run-state header *before* ending a session.
- **Living playbook rule:** when QA discovers any new app surface or recurring lesson — a new page/route, feature,
setting, game state, notification type/action/channel, entry point, background/killed-state behavior, asset/art
placement, repeatable bug class, missed edge case, fragile route, confusing state, image/layout failure mode,
security angle, or anything else that should be checked every future round — update **this `ClaudeQAPlan.md`** in the
relevant pass before ending the chunk. Also add the matching row/cell to `ClaudeQACoverage.md` if it needs recurring
verification. **And update [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) when the
discovery is durable engineering truth** (a new architecture fact, data path, contract, gate, flow, or a fixed bug's
re-introduction risk) — the QA plan captures *what to re-test*, the manual captures *what the system is and why it's
fragile*; both are living and both get updated. Do this even after the immediate bug is filed/fixed so the lesson or
newly discovered surface is not lost to memory or git history.
- **Learn from every ESCAPED or DEEP bug — MANDATORY retrospective (do this automatically, not only when asked).**
Any bug that (a) **escaped a prior round**, (b) needed **non-obvious diagnosis** (a crash, an "opens-and-closes",
a "didn't work", an intermittent, a wrong-root-cause first guess), or (c) **recurred** triggers a short retrospective
the moment it's fixed — the fix is **not complete** until all four are done:
1. **Add the guard that would have caught it** — a new `qa/` smoke check, a coverage row, or a concrete pass step
(e.g. the cold-start bug → `qa/entrypoint_smoke.sh`). If an existing smoke missed it, extend the smoke.
2. **Capture the lesson in its ONE canonical home, then link by ID elsewhere — never paraphrase it twice.** Split by
purpose: the **reflex** (how to *find* this class next round) goes in the relevant Pass of **this doc**, written
*generalized* and citing the bug ID as an example (do NOT re-narrate the bug here); the **substance** (root cause +
where it lives now + re-introduction risk + the guard) goes in
[`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md) → [Known landmines and recent
fixes](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes) (and update the matching
architecture/gate/flow section if the fix changed it). The manual is the next engineer's first read; a landmine
that isn't in it will be re-introduced. **Do NOT copy the fix into `memory/`** — per the memory rules, memory holds
only cross-session facts NOT in the repo (emulator↔account map, admin tooling/commands, standing auth,
never-commit); past fixes belong to the manual, so memory just points to the landmine ID if needed.
3. **Name the missing state/angle/entry-point** that let it hide and add it to the multi-angle / state matrices so it's
exercised every round (e.g. "real notification tap on an `am kill`'d app", not just `am start`).
4. **Note any wrong turn in diagnosis** so the misstep isn't repeated (e.g. "synthetic test passed while the real
path crashed → don't fix by reasoning; reproduce via the real channel + read the stack").
This is how the plan self-improves between rounds — treat the human pointing out a missed bug as a signal the plan had
a gap, and close the gap here, not just the bug.
- **Commit cadence**: commit `ClaudeReport.md` + `ClaudeQACoverage.md` after each pass and each chunk.
- **Chunking**: run small chunks (Pass C one screen-group; Pass A one feature), checkpoint after each.
- **Session-start ritual**: (1) read run-state header + both MD files; (2) `adb devices` shows **both** emulators
online; (3) **installed build == current HEAD** (rebuild+reinstall if unsure — never QA a stale APK); (4) continue
at the first `todo` / unverified-fix; (5) if a prior chunk left an active/stuck game session, recover it via in-app
"End their game" (log if needed), then redo that chunk.
## Batch sizing — sub-batch each pass to ONE context window (Round-1 calibration)
A pass is a **category**, not a unit of work. Execute each pass as **sub-batches (chunks)**, where a chunk = the
**largest coherent unit that reliably finishes AND commits within one context window, with margin**. End every chunk
with a commit + run-state update. If a chunk starts overflowing, split it; if chunks feel trivial, merge them.
**Why:** in Round 1, A & D fit as single batches, but B/C/E were too large → got cut off → deferred. Sub-batching
prevents half-done/lost work and gives cleaner per-chunk verification + revertable commits.
Default small: if a chunk requires two-device live driving, screenshots/montage review, logcat checks, or admin/API
verification, keep it to **one small route family, one game phase, or one notification type**. A chunk is too large if
it cannot produce a precise coverage update, issue log, and commit before context gets tight. Split before starting
rather than leaving a half-tested matrix behind. **Prefer Claude-friendly micro-batches**: smaller chunks let the agent
fully inspect screenshots, tap every CTA, vary app states, update files accurately, and avoid shallow "covered" rows.
| Pass | Chunk granularity | ~chunks |
|---|---|---|
| A Premium | one gated-feature family per chunk if live toggles are needed; otherwise free-state sweep → couple-shared verify | 24 |
| B Games | **one game per chunk max**; split complex games into lifecycle/playthrough chunk + join/resume/results/notification-entry chunk | 714 |
| C Visual | **one small route family per chunk** (both themes, ~23 screens/states, screenshots reviewed + nav/back + image-fit + all CTAs for that family) — never "all screens" or a broad tab at once | 1625 |
| D Security | one security assertion group per chunk: D1 at-rest · D2 rules static · D3 live negative raw API · D4 keys/recovery · D5/D6 leaks · D7 migration | ~6 |
| E Notifications | **one notification type per chunk** with the full contract below; split a type into direction/state subchunks if needed, but do not mark the type pass until both clients + source screens + fg/bg/killed + stale/malformed + payload/back-stack are covered | 1630 |
| F Resilience | **one dimension per chunk** (concurrency · lifecycle/process-death · network · time · account-lifecycle) | ~5 |
| G Account creation | **one creation/abuse dimension per chunk** (happy/validation · duplicate/conflict · fake-account abuse · lifecycle) | ~4 |
| H Branding | **one small route family per chunk** (~23 screens/states) consumer brand walk + ready-to-paste art prompts + existing-image integration verdict | 814 |
| I Performance | **one route-group per chunk** — gfxinfo/jank + read-count instrumentation (build the route smoke checklist) | ~3 |
| J Accessibility | **one a11y setting per chunk** (font scale · TalkBack · contrast · targets · keyboard · reduce-motion) | ~5 |
Context-cost tips: prefer **code/admin-read audits** (cheap) before live UI sweeps; **montage** screenshots
(dark|light pairs) to review many at once; keep one chunk = one TodoWrite focus.
## Guardrails & efficiency
- **Never `pm clear` / wipe app data** — breaks the App Check debug token. Pre-pairing QA: sign-out → fresh sign-up.
- **Never run `seed/build_db.py`.** Admin seeds/writes, entitlement toggles, and any deploys are **user-authorized per occurrence**.
- **By-design vs bug:** if a finding may be intended behavior, **log it and keep going** (don't stop to ask; don't unilaterally rewrite deliberate design — the log captures it).
- **Pass C parallelism:** set **5554 = Dark, 5556 = Light** to capture both themes at once.
- Never log decrypted message/answer content.
## Severity scale (label every issue)
- **P0 Critical** — crash/ANR, data loss, encryption/security leak, feature fully broken, premium bypass.
- **P1 Major** — feature partly broken, premium not unlocking for partner, wrong/missing notification, dead-end nav.
- **P2 Minor** — readability/contrast, clipping/overflow/truncation, theme not adapting, inconsistent styling, wrong/double-back navigation.
- **P3 Polish** — spacing/alignment/copy nits.
## QA passes (Round 1 = baseline)
### Pass A — Couple-shared premium (target: either partner premium → both unlock)
Test each gated feature in 3 states: **neither** premium → locked + paywall; **partner-only** premium → BOTH unlock;
**self** premium → unlock. Toggle Sam premium, confirm QA (free) unlocks; toggle off.
Features: Play-hub games (Desire Sync + any premium-badged), Connection Challenges, Memory Lane; Question Packs;
Spin the Wheel / Category Picker / Wheel History (+ any premium wheel categories); Date Match / Plan Date / Date
Builder; chat media + reactions + any premium chat tools (regression — already couple-shared); Subscription/Settings
reflects entitlement.
Gated files (for the fix): `ui/play/PlayHubViewModel`, `ui/desiresync/DesireSyncScreen`,
`ui/wheel/{CategoryPicker,SpinWheel,WheelHistory}*`, `ui/questions/QuestionPackLibrary*`,
`ui/dates/{DateMatch,DateMatches}Screen`, `ui/memorylane/MemoryLaneScreen`, `ui/challenges/ConnectionChallengesScreen`.
Also: **any VM/screen calling `EntitlementChecker.isPremium()` directly** (grep for it) is a candidate gate.
- **ENFORCEMENT, not just a checker-usage grep (mandatory — RETROSPECTIVE from A-201, R12).** A feature can carry an
`isPremium` **content flag** + a cosmetic `PremiumBadge` with **NO gate at all** — that's exactly how Date Match
shipped a premium **bypass** (free users could view/like/match ★Premium date ideas; `getDateIdeas()` returned
`DateIdeaSeed.all`, no `CouplePremiumChecker`, badge only). Prior rounds missed it because the audit grepped for
`CouplePremiumChecker` *usages* and found the gated features, never noticing the feature that had **no** checker.
So every round: (1) **grep for `isPremium` / `PremiumBadge` / premium content flags** (`DateIdea.isPremium`,
`category.access=="premium"`, `challenge.isPremium`, …) and for **each** confirm a real enforcement path exists —
a `CouplePremiumChecker` filter OR a paywall-on-interaction — **not just a badge**; (2) **actually TRY TO USE the
premium content as a free user** (like/open/play it), don't just confirm the lock renders — "badge shows" ≠ "gated".
A badge with no enforcement = **premium bypass** (P1+). Inspection lesson: *"shows a Premium badge" is a display
fact, not a gate; prove the gate by using the content while free.*
### Pass B — Games lifecycle (MANDATORY: play each game ONE complete time through ALL different play stayles of the game)
Games: This or That, How Well Do You Know Me, Desire Sync, Connection Challenges, Memory Lane, Spin the Wheel, + Date Match.
- **PLAY AS THE USER (mandatory mindset for this pass):** drive every game **the way a real user would** — reach it
through the actual in-app navigation a person would tap (Play hub → the game's card → its buttons), **not** via
deep-links, admin pokes, forced state, or any shortcut a user doesn't have. **Expect what the user expects:** if a
tap/button/flow doesn't do the obvious thing, or a screen doesn't behave the way a normal user would assume, **that
itself is a finding** — log it.
- **When something doesn't work: REPORT FIRST, then a minimal workaround (in that order).** Do **not** silently
engineer around breakage by taking extra steps the user wouldn't take. The moment the natural user path fails:
(1) **log the issue** in `ClaudeReport.md` with severity + the exact user action that failed and what was expected;
(2) **only then** apply the smallest workaround needed to keep the pass moving. The workaround **never replaces**
the report — a flow that needs a workaround to proceed is, by definition, broken and must be filed to fix. If a
workaround is impossible, mark the game `fail→<id>` (blocked) and continue with the next.
- **A launch/crash check is NOT sufficient. Each game MUST be played one full way through, end-to-end, on BOTH
devices** — start → answer/interact through **every** step/round/question on each device → reach the
**finish/reveal/results** screen → confirm the result renders correctly for both partners. Verify each
intermediate screen and interaction works (selections register, progress advances, both-answered gating,
reveal/scoring/summary correct). Premium games (Desire Sync, Memory Lane) need a premium toggle to play.
- The session lifecycle is exercised by the real playthrough: `status` active→completed; reveal/results correct on both.
- **GAME JOIN PATHS (mandatory — the second partner must JOIN, not just co-play):** the starter begins from real
in-app nav; the joiner then enters from **every** user-facing entry point — notification tap, Play-hub active state,
Home active-game card, Today prompt, waiting-room/resume screen, in-app foreground banner, game history/replay, and
(after the natural paths) deep-link/crafted intent + cold-start from a push. A game isn't complete unless **both**
partners can **start, join, resume, finish, reopen results, and recover from a stale/ended session** — with no
duplicate sessions, wrong routes, stuck waiting screens, broken back nav, or premium-gate mistakes.
- **FIRST-FINISHER → WAITING-PARTNER NOTIFICATION (mandatory state — async games):** explicitly exercise the asymmetric
state where **one partner finishes their part and the OTHER is idle/away**. The waiting partner MUST get a "your turn
to play" nudge (`partner_completed_part` via `onGamePartFinished`) the moment the first finishes — async games
(this_or_that / wheel / how_well / desire_sync) only flip to `completed` (→ `partner_finished_game`) once BOTH answer,
so without the first-finish nudge the waiting partner is told nothing. Verify the **idle partner** (on Home, or
backgrounded/killed) actually receives + can tap into the game. (This state was missed for a long time precisely
because QA always played both sides through; "one finishes, the other never played" is its own required angle.)
- **VARY THE STYLE OF PLAY (don't just repeat the happy path):** across runs, deliberately exercise *different* ways a
real couple would play each game, because different inputs hit different code paths:
- **Different DEPTHS and QUESTION COUNTS — cover the matrix, don't settle for one combo:** play each game across
**every depth/mood** (Light, Everyday, Deep, All-topics/shuffle) AND **every round length / number of questions**
(5 / 10 / 15), in *different pairings* across runs (e.g. Light×5, Deep×15, Everyday×10, All×5) — short *and* long
sessions, shallow *and* deep content. Different depths surface different question sets, tones, and edge content
(e.g. Deep/Desire-Sync sensitive prompts); different counts stress pacing, progress, and the both-answered gate.
Also exercise **each distinct answer type** (A/B, Yes/No, True/False, 15 scale, multi-select, free-text).
- **Different answer *patterns* that change the result** — all-match vs all-mismatch vs partial; both-yes vs both-no
vs split (so reveals show "shared", "all private", "0 matches", "perfect/zero score" — verify each renders right).
- **Different turn orders / who-starts** — partner A starts vs partner B starts; the guesser opens before vs after
the subject finishes; both open simultaneously (race); one device much slower than the other.
- **Different exit/resume styles** — finish normally; quit mid-game; background mid-game then resume; cold-kill
mid-game then reopen; "End their game"; re-open a completed session for the replay/results; play two games
back-to-back, and a *different* game type immediately after.
- **Edge inputs** — submit with nothing selected (should be blocked), rapid double-taps on answer/confirm/next,
spamming the start button, tapping during the reveal animation, switching tabs mid-game, receiving/tapping a
notification mid-game. None should crash, duplicate, or desync.
- Edges: re-open a completed session, leave mid-game (resume), no stuck session, no crash, logcat clean.
- Game start/finish pushes (`onGameSessionUpdate`) exercised here; full delivery/deep-link audit in **Pass E**.
- **Media permissions** (CAMERA, RECORD_AUDIO): granted works, denied degrades gracefully.
- **Done = every game has one verified complete playthrough** (a launch-only "opens, no crash" row is `partial`, not
`pass`). Coverage row format: `game × starter × join-entry × premium-state × depth/count × lifecycle-edge × result`;
only `pass` when start/join/play/finish/reopen/recover are all verified.
### Pass C — Visual pass, light + dark, ALL screens
Every route in `core/navigation/AppRoute.kt` (~50), in **both** modes: text contrast/readability (no invisible/
low-contrast), no clipping/overflow/ellipsis breakage, icons visible, backgrounds adapt, controls legible. Groups:
auth/onboarding/pairing (fresh acct); Home (solo + paired); Play + every game; Today + reveal/history; Messages
(inbox + conversation); Packs; Dates (Match/Builder/Matches/Bucket List); Wheel (picker/session/complete/history);
Settings + all sub-pages (Account, Notifications, Appearance, Privacy, Subscription, Relationship, Security, Delete
Account); Paywall; Your Progress/Activity; Recovery.
- **Images must belong to the screen:** during the UI sweep, visually inspect every illustration, glyph, banner,
empty-state image, pack art, celebration asset, and dark/light variant in context. It should feel intentionally
integrated with the page hierarchy, copy, spacing, and action area — not like a forgotten placeholder dropped into
an empty slot. Check crop, scale, padding, alignment, corner radius, background/tile treatment, theme variant,
**edge treatment**, loading/fallback state, and whether the image competes with or clarifies the primary task. If it is
broken, clipped, low-contrast, off-brand, stale, or placeholder-looking, file a bug in `ClaudeReport.md`; if the screen
works but would benefit from new/better art, log the prompt need in `ClaudeBrandingReview.md`.
- **SOFT EDGES — art must fade into the screen, not show a hard tile edge (mandatory):** every displayed illustration
should **blend/feather softly into the background**, not sit as a hard-edged rounded rectangle/card with a visible
boundary or border line. Inspect each illustration's edges against the screen on **both themes** — a crisp tile edge,
outline/border, or a pale block floating on the surface is a finding (C-ART-EDGE-001). (**Fixed R11:** `BrandIllustration`
now feathers its 4 edges to transparent via `Modifier.graphicsLayer{compositingStrategy=Offscreen}` + `drawWithContent`
`BlendMode.DstIn` linear gradients — `clip`+`border` removed — and `EmptyState` routes its illustration through
`BrandIllustration`, so all tiled art melts into the surface. Recurring check: verify it still holds and that any NEW art
helper / direct `painterResource` tile also feathers.) Fix pattern (if it regresses): feather the edges to transparent,
or a vignette matching the surface, or ship transparent-edged art — applied in the shared `BrandIllustration`/`EmptyState`
helpers so it's consistent everywhere.
- **Probe:** `ui/theme/Theme.kt` hardcoded brand colors + chat's custom `closerBackgroundBrush` — verify dark mode
truly adapts; grep screens for hardcoded `Color(0x...)`.
- **THEME-VARIANT ART must follow the IN-APP theme, not just the system (mandatory — RUN THE DECOUPLED STATE):** the app
has its own theme toggle (Settings → Appearance → Light/Dark/Device) that swaps Compose colors but does **not** change
the Android config `uiMode`, while `-night` drawables (`drawable-night-nodpi/`) and `painterResource` resolve off the
**system** `uiMode`. So art can mismatch the UI when the two disagree. **Test the decoupled state explicitly, every
round:** force system light then set the app to **Dark**, and force system dark then set the app to **Light**, and on
every screen that has a dark art variant confirm the illustration matches the **in-app** theme (no bright/light tile on
a dark screen, no dark tile on a light screen). Commands:
`adb -s <serial> shell cmd uimode night no` (system light) / `… night yes` (system dark); then toggle the in-app theme
in Appearance. Screens with `-night` variants to check: Security (privacy_recovery), Memory Lane, Bucket List, Answer
History, Date Match (empty + success), Connection Challenges header, Pairing success, Messages empty, Past Games,
Quiet-hours, Account-deletion, + any new `illustration_*` added to `drawable-night-nodpi/`. **Restore `cmd uimode night
auto` after.** Light art on a dark screen (or vice-versa) when the in-app theme is switched = bug (P2 theme-not-adapting;
see C-DARKART-001). (**Fixed R11:** `CloserTheme` provides `LocalAppInDarkTheme`; `BrandIllustration` loads each drawable
through `context.createConfigurationContext(cfg)` whose `UI_MODE_NIGHT_*` is set from `LocalAppInDarkTheme`, so the
`-night` variant follows the IN-APP theme, not the system. Verified live R11 both decoupled directions. Recurring check:
re-run the decoupled state and confirm it still holds, including any newly added `-night` art.) Fix pattern (if it
regresses): drive the resource `uiMode` from the in-app theme as above, or `AppCompatDelegate.setDefaultNightMode`/config
override, so `painterResource` picks `-night` per the app's own setting.
- **States, not just happy path:** empty / loading / error / not-paired / locked-premium / signed-out /
stale-or-deleted-target / populated-with-many where they exist; many need data setup (seeding is user-gated) — note
unreachable states in coverage rather than skipping silently.
- **Text/data stress:** test long names, long relationship labels, long question/answer text, emoji, multiline content,
empty optional fields, many list items, and both partners having similar names. Verify no clipping, overlap,
confusing attribution, broken sorting, or hidden actions.
- **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**.)
- **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
AND from history AND from `partner_answered`. A screen that works from one entry but breaks/duplicates from another = bug.
- **Every link, CTA, and mission must prove its destination:** actively hunt for dead buttons, wrong targets, generic
Home fallbacks, no-op taps, stale routes, and confusing affordances. Example class: a Reveal card saying
**"Tiny Mission: Send one flirty text"** must open the relevant Messages/conversation flow, not do nothing. For every
button/card/chip/row, record the expected destination before tapping, then verify the actual destination, state,
payload, and back stack. Broken/no-op/wrong-destination CTA = bug (usually P2; P1 if it blocks a core flow).
- **All routes into a game / join-game state (verify each opens the correct game + session + partner-state + mode +
premium/couple-entitlement + back stack):** Play-hub cards (incl. premium-gated), active-session banners, Home/Today
game prompts, game history, replay/results, waiting screens, notification-opened screens, in-app banners,
"join/resume/continue/view results/end (their) game", deep-link/crafted intent, and bottom-tab return into an active
game. Wrong/duplicate destination, double-back, stale-session join, dead-end, or a route that bypasses the
premium/couple check = bug.
- **TAKE EVERY AVENUE (exhaustive nav fuzzing — actively hunt for nav bugs, don't just walk the happy path):** treat
navigation as something to *break*. On every screen, **tap every interactive element** — each button, card, row,
icon, chip, link, tab, header back-arrow, system back, and any "see all / history / edit / manage" affordance — and
follow where it goes. Then try the *combinations and sequences* a curious user hits:
- **Every order:** switch bottom tabs in many orders, mid-flow (open a game, jump to Messages, come back); enter a
deep screen then tab away then back; open A→B→C then back-back-back.
- **Rapid / repeated input:** double- and triple-tap navigation targets (especially "open game", "Play now",
"Create/Start session", notification taps) to surface double-push/duplicate-screen/stale-route bugs (cf. B-004).
- **Interrupt mid-navigation:** background/rotate/lock during a transition; tap a notification while already on that
screen, on a different screen, and while logged-out/unpaired; cold-start straight onto a deep link.
- **Dead-ends & traps:** from *every* screen confirm there's always a way out (back/close/home) — no screen that
strands the user, needs two backs, exits the app unexpectedly, loops, or lands blank. Re-check the asymmetric-game
waiting screens, replay/results screens, and paywall specifically.
- Log **every** wrong/duplicate/dead destination with the exact tap sequence to reproduce. Wrong/double-back or
dead-end = **P2** (P1 if it traps the user or loses their progress).
- **Back-stack / "double back":** from every entry point, **system back AND the in-app back arrow** return to the
correct previous screen — no dead-ends, no exiting the app unexpectedly, and **no screen that requires pressing
back twice** (duplicate/stacked destinations on the back stack = bug). Bottom-tab reselection and deep-link/
notification entries must land with a sane back stack (back → Home, not off the app or a blank screen). Wrong/
double back or a dead-end = **P2** (P1 if it traps the user).
- **UI consistency / polish defects:** compare each screen against sibling patterns in the same area and across the
app. Headers, labels, status chips, partner names, connected-state copy, spacing, card treatments, and button
hierarchy should feel intentional and consistent. Awkward or out-of-place UI such as a Settings relationship row
where **"Connected with ..."** looks visually odd, cramped, misaligned, or unlike the rest of Settings is a finding:
file as a bug if it looks broken/inconsistent; log to `Future.md` only if it is purely a product/content improvement.
- **D1 At-rest coverage:** admin-read RAW docs/objects, assert ciphertext for every private type — chat text +
`lastMessagePreview` (`enc:v1:`), chat media bytes (Tink `01 69 59 51 f0…`), answers (`sealed:v1:`/`enc:v1:`),
date plans + `date_swipes`, Memory Lane capsules, Bucket List. Also: **wrappedCoupleKey** + recovery material never
plaintext; **invite code (KDF seed) never stored raw**; **no push payload carries private content**.
- **D2 Rules audit (static):** member-only reads, author/server-only writes, ciphertext enforced on every private
field, immutability, **no premium self-grant**, entitlements write:false; re-audit conversations/typing/reactions
+ entitlement partner-read; **no catch-all** `match /{document=**}`; list/query not enumerable; `get()`-rules don't
over-expose; **no legacy plaintext/downgrade path** (`coupleEncryptionEnabled` holds; no disabled-encryption branch).
- **D3 Negative access tests (EXECUTE LIVE via raw API — do not defer):** a **non-member** account is *denied* reading
messages/answers/dates/entitlements/sessions/capsules, writing plaintext to encrypted fields, self-granting premium,
and any cross-couple access. Run it the **raw-API angle**: mint a non-member ID token (admin custom token →
Identity Toolkit `signInWithCustomToken` REST) and issue Firestore REST GET/PATCH against the couple's docs — expect
App Check `403` or rules `PERMISSION_DENIED` on every attempt. Also issue the **same** reads with a **member** token to
characterize the enforcement layer (App Check vs rules). Any unauthorized `200` with couple data = **P0**.
- **D4 Key exchange / management / recovery (E2EE crux):** couple key client-generated, only leaves device **wrapped**
(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.
- **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.
- **D6 Leak vectors:** no private content in analytics/crash; `allowBackup=false` + backup rules exclude sensitive data;
deep links re-check membership; clipboard user-initiated; consider `FLAG_SECURE`; repo scan for committed secrets.
- **D7 Encryption migration:** test the `encryptionVersion` paths (0 plaintext → 1 migrating → 2 strict) on a legacy
couple — migration completes without exposing plaintext or losing/garbling old content, and a half-migrated couple
is safe (no mixed read failures, no downgrade). This is the riskiest data path for existing users.
### Pass G — Account creation, validation & fake-account abuse (MANDATORY — both the happy path AND the attacks)
Cover **every account-creation avenue a real user takes** and **every fake/abusive creation attempt an attacker would
try.** Use throwaway test accounts (sign-out → fresh sign-up; never `pm clear`). Report-first like every pass.
- **Real creation flows (happy path + validation):** sign-up (email/password and any social/anonymous path), profile
creation, and pairing — both **create-invite** and **accept-invite** sides. Verify field validation (invalid/empty
email, weak/short password, mismatched confirm, name length/emoji/unicode), the **error copy is friendly** (no raw
SDK/Firebase error leaking — cf. A-OBS), loading/disabled states, and that a brand-new unpaired account lands on the
correct "create or accept invite" home (not a broken/blank or paired view).
- **Duplicate / conflicting creation:** sign up with an **already-registered email** (clear "already in use", no crash,
offer sign-in); create a second account while one is signed in; re-run onboarding after completing it; accept an
invite while **already paired** (must be rejected cleanly); two devices accepting the **same invite** (single-use —
the second must fail gracefully).
- **Fake / malicious creation attempts (security — expect DENY, never crash or leak):** create an account that is
**NOT a member** of the test couple and attempt every cross-couple action (read messages/answers/dates/entitlements,
write to the couple, self-grant `premium`/`hasPremium`, join/hijack pairing with a guessed/expired/reused invite
code) — all must be **denied by rules** (this is the live execution of **D3**). Probe **invite-code abuse**: replay a
used code, use an expired code, brute-force/guess attempts (CSPRNG entropy + single-use + expiry must hold). Probe
**App Check**: a request without a valid token is rejected. Confirm a malformed/forged sign-up can't bypass profile
or membership requirements. **Any successful unauthorized create/read/write = P0.**
- **Account lifecycle around creation:** sign-out → sign-in (state restores, no stale couple); **delete account** then
re-create with the same email (clean slate, partner notified/unpaired); an unpaired/just-created account tapping a
stale notification or deep link is handled gracefully (no crash, sane landing).
- **Done = every creation avenue exercised** (happy + duplicate + malicious) with each attack **denied** and each happy
path validated end-to-end; findings filed with exact repro.
### Pass E — Full notification suite, deep-links & join-game navigation (every type, both clients, every app state)
Run the **complete** suite across **both clients** (QA→Sam AND Sam→QA). Each type verified end-to-end: **trigger fires
→ delivered to the right partner (never self/non-member/ex-partner) → correct channel + copy with no private content →
tap opens exactly the right item (loaded, not generic Home/dead-end) → sane back stack → privacy/authz re-checked on
open**. No duplicates; rate limiter (20/day, 100/week) doesn't drop legit ones.
- **Notification chunk contract (small chunks, complete coverage):** each chunk owns **one notification type** (or one
explicit subchunk of that type, e.g. `chat_message QA→Sam foreground/source-screen sweep`, then
`chat_message Sam→QA background+killed+stale`). Before starting, write the chunk's matrix in `ClaudeQACoverage.md`;
after finishing, mark each cell `pass | fail→id | blocked→id | not implemented→Future.md`. A notification type is
not complete until all applicable cells below are covered:
- **Directions:** QA→Sam and Sam→QA; sender must not receive their own push unless intentionally designed.
- **Process states:** foreground, background/warm, killed/cold-start, force-stopped if deliverable, screen locked,
and resumed after rotation/process recreation when relevant.
- **Current screens:** Home, Play hub, active game/waiting/results, Today/reveal, Messages inbox, exact conversation,
Settings/sub-settings, Paywall, unrelated deep screen, logged-out, unpaired, and stale prior-partner context.
- **Entry surfaces:** foreground in-app banner/head, Android system tray tap, any push action button, crafted
deep-link/intent matching the payload, repeated/double tap, and tap after the target has changed.
- **Targets:** fresh target, already-open target, completed target, stale/expired/deleted target, unauthorized target,
wrong couple/session/item ID, malformed/missing extras, and no-network-on-open.
- **Assertions:** correct recipient, correct channel/priority/copy, no private payload/log content, exact destination,
membership/auth/entitlement re-check, no duplicate route/session, sane back stack, logcat clean, and coverage/docs
updated before the chunk ends.
- **Notification tap crash triage (mandatory):** never conclude "the notification didn't open" from UI behavior alone.
Before each notification/deep-link tap, clear or timestamp logcat; after the tap, inspect both devices for
`FATAL EXCEPTION`, ANR, ActivityTaskManager errors, `RuntimeException`, navigation/deep-link exceptions,
`PERMISSION_DENIED`, and swallowed repository/decryption errors. If the app returns Home, stays put, flashes,
restarts, or silently fails, classify whether it was wrong routing, missing extras, stale data, permission denial, or
a crash. Any notification tap that crashes (example class: tapping a game notification to open **Spin the Wheel**)
is a filed bug with stack trace + exact payload/session/game type, not a vague "didn't open" note.
- **Test the REAL launch path, not a synthetic one.** `adb am start … --es type=…` does **not** reproduce a real
notification tap: the OS notification tap launches the activity through the **SysUILaunch splash handover**
(`reportSplashscreenViewShown` → `handOverSplashScreenView`), which `am start` skips. A whole bug class
(e.g. the **splash-exit `provider.iconView` NPE** — the handover delivers a splash view with **no icon**,
`SplashScreenView: Icon: view: null`, on notification cold-starts only) crashes onCreate → "Force finishing
activity" → the app **opens-and-closes**, yet `am start` AND the normal launcher icon both pass. Verdict: for
cold-start/notification routing, a synthetic-intent pass is **not** a pass — confirm with a real push tapped from
the shade on an `am kill`'d app.
- **"Opens and closes / flashes / returns to launcher" ⇒ assume a crash; pull the stack FIRST.** `logcat -c`
before the tap, then grep `FATAL EXCEPTION|AndroidRuntime|Force finishing|getIconView`. A real repro + the stack
trace beats code-reasoning every time (this bug was misdiagnosed as deep-link routing until the live stack named
`MainActivity.kt` + `SplashScreenViewProvider.getIconView`). Confirm crashes reach **Crashlytics** so field cold-start
crashes surface.
- **Many notification types "broken" at once ⇒ suspect the SHARED entry path (splash/`onCreate`/launch), not each
handler.** When chat AND every game's results push all fail identically, the bug is in what they share (the
cold-start path), not per-type routing. Re-run a **cold-start smoke after ANY change to** `MainActivity` / splash /
theme / manifest / launchMode / branding-"loading state" commits — these cosmetic-looking changes broke the launch.
- **For "worked before, broken now": `git blame` / `git log -L` the crashing line/function** to pin the introducing
commit, then re-test that exact path on it.
- **Both-client × app-state matrix (per type):** QA→Sam and Sam→QA, each in **foreground / background / killed
(cold-start)**, plus **already on the target screen**, **on a different screen**, **logged out**, **unpaired**, with
a **stale/expired/completed/deleted target**, and **both users opening around the same time**. Not a `pass` unless it
works from both clients in every state that applies.
- **Current-screen/source-screen matrix (per type):** do not test notifications only from Home or only from a clean
launch. For each notification type, vary where the receiving client is when the notification arrives/taps: **Home,
Play hub, active game/waiting/results, Today/reveal, Messages inbox, exact conversation, Settings/sub-settings,
Paywall, an unrelated deep screen, app backgrounded from each major tab, and app fully closed/killed**. Foreground
banners, system-tray taps, warm-start `onNewIntent`, and cold-start launch must all route to the exact target. A tap
that lands on generic Home, stays on the old screen, opens the wrong tab, loses extras, duplicates the destination,
or needs a second tap is a bug.
- **Permission/token health:** cover Android `POST_NOTIFICATIONS` granted, denied, "don't ask again"/system-disabled,
and re-enabled states; Settings notification toggles; sign-out/sign-in token refresh; same account on two devices;
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.
- **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/
priority, no raw Firebase error/raw IDs, no private content in text/payload/logs/analytics/crash; (4) tap opens the
exact destination — specific conversation/session/capsule/match/question/settings/pairing, never blank, never a crash
on missing/stale/malformed/unauthorized data, no duplicate/stacked copies, completed→results/replay, expired/deleted→
graceful fallback; (5) back stack sane — back returns sensibly (Home/prev context), no double-back, no unexpected
exit/loop/blank; (6) deep-link re-checks auth + couple membership + pairing + entitlement + target ownership +
session status + existence — a non-member/logged-out/stale/unpaired open must NOT reach private content and must fail
gracefully.
- **Inventory (type → Cloud-Function trigger → recipient → destination)** — verify each; mark any unimplemented type
`not implemented→Future.md` (don't count as pass):
`chat_message`(onMessageWritten → partner → conversation; foreground→chat-head bubble) ·
`partner_started_game`/`partner_finished_game`(onGameSessionUpdate → partner → game/join · results/reveal) ·
`partner_completed_part`(**onGamePartFinished** → waiting partner → game; fired when the FIRST player finishes an
async game so the partner is told "your turn" — async games complete only when BOTH answer, so without this the
waiting partner got nothing between first-finish and both-finish) ·
`join_game`/`game_invite` & `partner_joined_game` (if present → partner/starter → join screen · waiting-room update) ·
`partner_answered`(onAnswerWritten → partner → reveal) ·
`game_abandoned`/`game_ended` (if present → partner → safe ended state, not a stuck session) ·
`daily_question`(assignDailyQuestion)/`daily_question_reminder`/`daily_reminder`(dailyQuestionReminder → Today) ·
`date_match`(createDateMatch → match) · `date_plan_update` (if present → date plan/builder/match) ·
`partner_joined`+`invite_created`(acceptInviteCallable → pairing/home) ·
`partner_left`(onCoupleLeave)/`partner_deleted_account`(onUserDelete → home/relationship settings) ·
`memory_capsule_unlocked`(scheduled → capsule) & `memory_capsule_created` (if present → Memory Lane/locked capsule) ·
`challenge_day_ready`(→ Connection Challenges) & `challenge_day_completed` (if present → challenge progress) ·
`outcome_reminder`(scheduledOutcomesReminder) · `reengagement`(reengagement/gameRetention) ·
`gentle_reminder`(sendGentleReminderCallable) · `spki`(key identity/confirm → security/key screen) ·
`subscription_entitlement_changed` & `security_recovery` (if present).
- **Game-notification suite (per game):** A starts from Play hub → B gets the start/join push (if supported) → B taps
and lands on the correct join/waiting/active screen → B can join from there → A sees B joined/answered → both finish
→ finish push opens the exact results/reveal → re-opening the push after completion opens replay/results (not a dead
active session) → if A ends/quits, B is notified or shown a graceful ended state → a **stale** game push routes to
results/history or a clear expired-session message → simultaneous start/join yields **one** session, neither stuck →
premium gate holds (neither-premium push must NOT bypass paywall; either-premium unlocks for both). For each game
type, including **Spin the Wheel**, notification taps must be paired with logcat review so crashes are caught even if
the visible symptom looks like a no-op or generic Home fallback.
- **Join-game navigation suite:** every entry that leads to joining/resuming a game opens the correct game + session +
partner-state + mode + entitlement + back stack — Play-hub card, active-game banner/card, Home active-game card,
Today game prompt, notification tap, in-app foreground banner, game history/replay, partner waiting screen, results/
reveal, "End their game"/stuck-session recovery, deep-link/crafted intent, cold-start from push, bottom-tab return
into an active game, any push action buttons, and any "join/resume/continue/view results/play again". No wrong game
type, no accidental stale-session join, no duplicate session on double-tap, back returns correctly.
- **Payload security (P0 on any hit):** inspect raw payload + logs — no plaintext message/answer/capsule/date-plan/
bucket-list/swipe content, no raw invite code/seed, no recovery phrase, no wrapped/decrypted key material, no
email/name unless intentionally public; payload carries only the minimum routing metadata. Any private content = P0.
- **Malformed / stale intents:** fire crafted deep-links with missing/unknown type, missing/wrong target or couple ID,
wrong game type, expired/completed/deleted target, unauthorized couple/session, malformed params, duplicate/rapid
taps, a push for another user/previous partner, while logged-out/unpaired, while on the target screen, and during a
different active game → never crash/leak, always a graceful fallback + sane back stack.
- **Scheduled/time-based:** trigger manually (invoke callable/function or seed the due condition — user-gated).
- **Foundations:** FCM token registration on sign-in (`TokenRegistrar`) + `onNewToken` + token cleanup on sign-out/
account-switch; POST_NOTIFICATIONS prompt + denied path; channels (`di/NotificationModule`); deep-link routing
(`MainActivity.deepLinkRouteFromIntent` → `AppNavigation`); foreground/background split
(`core/notifications/AppMessagingService`); no duplicate local+remote notification.
- **Coverage:** record per row `type × trigger × recipient × app-state × destination × back-stack × privacy ×
both-client` in ClaudeQACoverage.md; only `pass` when delivery + routing + back-stack + privacy + both-client are all
verified. Missed delivery or wrong deep-link = P1; private content in any payload = P0.
### Pass F — Resilience, concurrency, lifecycle & time (cross-cutting; a 2-user realtime app needs these)
- **Concurrency / realtime races (two partners at once):** both answer the daily question simultaneously; both
start/join the same game; both swipe a date / react at once; one quits while the other submits; both tap a
notification at once; partner acts while you're mid-flow. No lost writes, no stuck state, no duplicate sessions,
reveal still correct. (This is where a couples app breaks.)
- **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.
- **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
**deep link**, and any widget/quick-action. This is the `MainActivity`/splash/`onCreate`/auth-bootstrap path; a crash
here (e.g. splash-exit `iconView` NPE) breaks **all** notifications at once. **Run `qa/entrypoint_smoke.sh` here every
round and after any MainActivity/splash/theme/manifest/nav/notification change.** Reproduce via the REAL push tapped
from the shade (not `am start`); "opens-and-closes" ⇒ pull the FATAL stack (see Pass E crash-triage).
- **Network resilience:** offline / flaky / airplane mid-action across answers, games, dates (not just chat media) —
graceful failure + retry/queue, no crash, no silent data loss, recovery on reconnect.
- **Idempotency / rapid input:** double-tap send/submit, rapid nav, double-start, double-join, repeated paywall-unlock
taps — guarded (no double-send, no duplicate session, no crash).
- **Time-dependent behavior:** daily-question rollover (6 PM CST assignment), streak day-boundary + repair window,
capsule unlock times, reminder schedules, challenge-day availability, timezone change — test across a date change
(manipulate device clock / trigger functions).
- **Account/couple lifecycle:** brand-new (empty) account; unpaired state; pair → unpair → re-pair; partner leaves
mid-session; account deletion cascade; same account on two devices; stale notifications after unpair/delete are
graceful; invite accepted while already paired is rejected cleanly. No orphaned/broken state.
- **Install/update/migration lifecycle:** fresh install, update over an existing signed-in install, app data retained,
Room/DataStore/SharedPreferences migrations, notification channel migration, cached encryption/key material,
pending deep links/notifications across update, and version-skew between partners if one device updates first. No
sign-out loops, stale build routing, lost local state, broken permissions, or migration crashes.
- **Crash reporting:** confirm crashes/ANRs are actually captured (Crashlytics) so field issues surface.
### Pass H — Branding & artwork (every screen: could it carry more of the brand? where would art help?)
A consumer-mindset pass focused on **brand presence and delight**, not defects. Walk **every screen and surface** and
ask: *does this feel like Closer (private, warm, equal, intentional — a ritual for two)? Could brand color, the heart
mark, a brand message, or an illustration make it warmer or clearer without clutter?* Output is **artwork descriptions
written as ready-to-paste ChatGPT image-generation prompts** — the user generates the images; we only describe them.
- **Existing art integration check:** judge the art as part of the whole page, not as a standalone asset. Confirm each
image supports the screen's job, aligns with the surrounding typography/actions, has enough breathing room, and uses
the right light/dark treatment. Art that looks generic, unfinished, randomly placed, or visually disconnected is a
finding even if the bitmap itself is technically valid.
- **Soft edges (art melts into the surface):** illustrations should **fade/feather into the screen background**, not read
as a hard-edged tile/card with a crisp boundary or outline. Confirm edge treatment on both themes; a hard tile edge is
a finding (C-ART-EDGE-001). Generated art should carry **transparent/feathered edges** (no baked-in rounded-rect block);
if rendered, the shared helper should fade the edges to the surface. Record the desired edge treatment in each prompt.
- **First, lock the house style (do this once per round, refresh if the art evolved):** read `docs/brand/visual-identity.md`
+ `docs/brand/asset-system.md` AND open 23 existing illustrations (`illustration_couple_onboarding`,
`illustration_reveal_celebration`, `pack_art_*`) to capture the *actual* look. New screens/features since the last
brand review must be folded in. Keep the canonical **house-style prompt prefix** + palette in the branding deliverable
(`ClaudeBrandingReview.md`) so every prompt reuses it and **all generated art matches the existing artwork.**
- **House style (must hold for every prompt):** flat 2D pastel vector illustration; soft rounded shapes, no harsh
outlines, gentle gradients; palette aubergine `#24122F` / deep purple `#56306F` / lavender `#B98AF4` / soft pink
`#F7C8E4` / soft lavender `#D9B8FF` / blush white `#FFF8FC`; motifs = two-equal-halves heart, paired/sealed cards,
floating hearts + petals, candle/mug/lavender-sprig warmth, moon/quiet-hours, calendar/date-card, capsule; mood =
warm, quiet, equal, intentional. Couple figures balanced + inclusive, faces simple. **Never** show readable answer/
prompt/message text, invite codes, emails, dating-app clichés, stock photos, alarm/urgency/surveillance imagery.
- **Per screen, decide the brand opportunity** (pick the lightest that fits — don't over-decorate):
- none needed (already on-brand, or a dense list/form where art would clutter) — say so;
- **color/typographic** brand touch (palette, heart mark, a rotating privacy message);
- **small glyph** (brand glyph for a relationship concept — describe it for the glyph set);
- **hero/empty-state/celebration illustration** (the high-value case → write the full ChatGPT prompt).
- **Each artwork item records:** screen/route · placement (hero / empty / header / card / celebration) · why it helps ·
filename to match the existing scheme (`illustration_*`, `pack_art_*`, `glyph_*`, `particle_*`) · **the ChatGPT
prompt** (house-style prefix + the specific scene) · aspect ratio/size + light/dark behavior. Cross-check the
brand doc's "Needed additions" / empty-state list and **mark which already have assets vs still need art** (e.g.
Android may still lack illustrations that iOS has).
- **Prioritize** the screens a user feels most: onboarding/pairing, Home, paywall/subscription, reveal/celebration,
empty states (no messages/dates/capsules/history), Memory Lane, Connection Challenges, date match, quiet-hours.
- Branding *defects* (mis-colored, clipped, off-brand, low-contrast art) are bugs → `ClaudeReport.md`. Pure
"works but could be warmer / a feature idea" → `Future.md` `## QA`. New art to create → `ClaudeBrandingReview.md`.
### Pass I — Performance & route efficiency (jank, redundant reads, caching) [Future.md P14]
Before store polish, profile **every top route** and **every high-cardinality list** for jank, repeated Firestore
reads, missing cache use, and slow navigation. Drive each route as a user and instrument reads/frames.
- **Frame / jank:** scroll every long list (Messages inbox + conversation, Answer History, Question Packs, Past Games,
Wheel History, Bucket List, Date deck, Activity/Progress) and open every top route while watching
`adb shell dumpsys gfxinfo <pkg> framestats` (or Perfetto / Studio Profiler) — flag dropped/janky frames, slow first
frame, and `Choreographer: Skipped N frames` / main-thread stalls in logcat. Transitions/animations stay smooth (~60fps).
- **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.
- **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
perceptibly slow (>~300ms).
- **Deliverable:** a reusable **route smoke-test checklist** (every top route × {load time · jank · read count}),
captured as a runnable script so each round re-checks cheaply.
- **Remediation when found:** lazy-load/page large lists; cache local question/category data; dedupe + scope snapshot
listeners; skip redundant fetches on tab switches; add skeleton/loading states (cf. Future.md P8) over blocking spinners.
- Findings: real jank/leak/redundant-read = bug → `ClaudeReport.md` (P2; **P1** if it ANRs or leaks listeners, **P0** if
it drops data); "could be smoother / add skeletons" → `Future.md` `## QA`.
### Pass J — Accessibility (font scale · contrast · screen reader · targets · keyboard · reduce-motion) [Future.md P15]
Every **primary flow** must be usable with accessibility settings on. Enable each setting and walk the core flows
(auth, onboarding, pairing, Home, a full game, daily question + reveal, Messages, Paywall, Settings) end to end.
This is the deep home for a11y; the Pass C contrast/font spot-checks feed into it.
- **Font scaling:** `adb shell settings put system font_scale 1.3` (then 1.5, 2.0) — every primary flow stays usable:
**no clipped/overlapping text, no cut-off or hidden buttons/actions** (scroll where needed). **Acceptance: all primary
flows usable at increased font scale without clipped buttons or hidden actions.** Restore `font_scale 1.0` after.
- **Screen reader (TalkBack):** every interactive element has a meaningful semantics/`contentDescription` (icon-buttons
especially: back, send, like, close, the brand-mark loader, game option cards); decorative images are silenced
(`clearAndSetSemantics {}` / null desc); reading order is logical; no unlabeled "Button"; custom controls (spin wheel,
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).
- **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
order, IME/Enter actions work, focus is visible, no traps.
- **Reduce-motion:** with "Remove animations" (`adb shell settings put global animator_duration_scale 0`), the loader,
celebration particles, reveals, splash handoff, and transitions degrade gracefully and **no motion-gated content
becomes unreachable** (the loader/particles already honor this — verify everywhere). Restore to `1` after.
- **Remediation:** add semantics labels, raise touch targets, fix contrast tokens, guard motion behind the reduce-motion flag.
- Findings: missing label / clipped-at-large-font / sub-48dp / failing contrast = bug → `ClaudeReport.md` (**P2**; **P1**
if it blocks a primary flow for assistive-tech users); polish → `Future.md` `## QA`.
## Reporting → ClaudeReport.md (living QA report)
- Header: date, build, devices, round number + run-state header.
- One section per pass (AJ), each a table: **ID | Area | Screen/Route | Mode | Severity | Description | Repro
| Evidence | Suggested fix | Status**.
- Summary: counts by severity. Report only during passes — no fixes recorded until the fix phase.
### Report hygiene — keep it CLEAN, lean, and never dangling (the report is a *current-state* doc, not an archive)
The report's job is to show, at a glance, **what's wrong right now** — not to accumulate a history of everything ever
fixed. Stale fixed rows and stacked old run-states make it unreadable and hide the real signal. So:
- **A Fixed row survives exactly ONE confirmation round, then it's removed.** When you fix an issue, mark its row
`Fixed` (with the commit) and keep it through the **next** re-QA round. Once that round re-verifies it, **delete the
row** — the full root-cause/fix detail already lives in the **commit message** (the row cites the hash), so nothing is
lost. Don't carry confirmed-fixed issues across multiple rounds.
- **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.
- **Open issues first; resolved issues compact.** Order every pass section **open (P0→P3) on top**; keep a short
`Resolved & confirmed (archived — detail in git)` line listing only the **IDs** of older fixed-and-verified issues
(not their tables). The big per-issue tables exist only for **currently-open** and **fixed-this-round-pending-confirm**
issues.
- **Severity board reflects NOW.** One board, current counts; `Open` is the number that actually matters. When `Open`
hits 0 at every level, the report should be **short** — current run-state, a 0/0 board, the archived-ID line, and the
operational constants (devices/accounts, standing-auth, playbook pointers). If it's long while everything is fixed,
it needs pruning.
### Coverage-matrix hygiene (`ClaudeQACoverage.md` — a *current-status* matrix, not a per-round changelog)
- **Flip, don't stack.** When a fix is confirmed, change that row's `fail→id` to `pass` and move the ID to an archived
line — never leave a confirmed-fixed `fail→id` dangling, and never keep a contradicting "still owed" note next to a
completed row.
- **One status per cell, current.** Each screen/feature/game/notification shows its **latest** status only; collapse
prior rounds' narration into a single one-line **round history**. Keep an at-a-glance pass-status table at the top.
- **Keep the resume signal sharp.** What a returning session needs is *what's left* — surface `todo`/`deferred`/
`blocked` items plainly; don't bury them under superseded prose.
### Extremely-easy-to-read mandate (applies to ClaudeReport.md, ClaudeQACoverage.md, and Future.md)
Optimize every QA doc for a reader who has **5 seconds** to find the current state:
- **Lead with the answer.** Top of the file = current round + the one-line verdict (e.g. "0 open P0P3; security clean")
before any detail.
- **Tables over prose** for issues; **short rows**. Put long root-cause analysis in the **commit**, not the row — the
row gets a one-sentence description + repro, then the commit hash.
- **No walls of text.** Break run-state into scannable lines; bold the few words that matter; no multi-paragraph
headers. If a paragraph is longer than ~3 lines, it's probably commit material, not report material.
- **Consistent shape every round** so a returning reader (or a post-compaction resume) finds things in the same place.
## Fix phase (only AFTER all passes of the round complete)
- Work strictly by severity: **all P0 → P1 → P2 → P3**.
- **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** + **commit** (one per issue/cluster) → next. 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**,
not a synthetic shortcut. A crash/launch/notification fix is only "Fixed" once reproduced-then-cleared via the REAL
channel (real push tapped from the shade on an `am kill`'d app; real launcher cold-start) — `am start`/`am force-stop`
passes don't count. For any cold-start/notification/launch fix, the gate is **`qa/entrypoint_smoke.sh` green**. (This
session's miss: a routing "fix" was declared on `am start` evidence while the real bug was a splash crash on the FCM
cold-start. Don't repeat it.)
- **Couple-shared premium fix**: replace direct `isPremium()` gates with
`CouplePremiumChecker.coupleHasPremium(partnerId)` in every gated VM/screen (partner-entitlement read rule deployed).
**High regression risk** — re-verify each feature in BOTH self-premium and free states.
- Gated actions (entitlement toggles, deploys) are **user-authorized per occurrence**.
- **New issues found while fixing** are logged (new ID), not silently fixed beyond scope — next re-QA round catches them.
**Definition of done:** a **pass** is done when every coverage row is `pass`/`fail→id`/`not implemented→Future.md`/
`blocked→id`; a **round** is done when all passes (AJ) are done; **flawless** = one full round with **zero open P0P2
and Passes D + E fully clean** (no open P0/P1 in I/J), **every game fully played through, every notification type
verified or explicitly `not implemented→Future.md`, all join-game navigation paths and all back-stack checks
verified**, **and `qa/entrypoint_smoke.sh` GREEN on both emulators (0 FAIL — every entry-point cold-start opens and
stays)**. Then stop (P3s optional). Don't re-open a clean pass within the same round.
## Re-QA loop (until flawless)
After the fix phase, re-run Pass AJ (regression + confirm fixes). Repeat **fix → re-QA** rounds until a full
round yields zero P0P2 and Passes D+E fully clean.
- **Prune on confirmation (Report hygiene):** the moment a re-QA round re-verifies a `Fixed` issue, **delete its row**
from `ClaudeReport.md` (move its ID to the compact `Resolved & confirmed (archived — detail in git)` line) and
collapse that finished round's run-state header. A fixed issue lives in the report for **one** confirmation round
only — never let confirmed-fixed rows or old run-states accumulate. See **Report hygiene** under Reporting.