docs(seed): replace question guides with v2 — content guide, rewrite plan, new quality checklist

This commit is contained in:
null 2026-06-25 18:48:37 -05:00
parent f47c8e2b64
commit 4686a2c200
8 changed files with 612 additions and 897 deletions

View File

@ -1,6 +1,7 @@
# Claude QA Coverage Matrix # Claude QA Coverage Matrix
> Resume anchor. Status: `todo | pass | fail(→id) | n/a`. See `ClaudeReport.md` run-state header for current position. > Resume anchor. Status: `todo | pass | fail(→id) | n/a`. See `ClaudeReport.md` run-state header for current position.
> **Round 6 (branding + Future.md regression) COMPLETE 2026-06-25, client `f47c8e2`:** new surfaces from `95cad84` (white-keyhole icons, animated chip+fill loader, splash, pairing hero) + `f47c8e2` (inclusive gender, turn copy, push-budget split, results-suppression `ActiveGameSessionMonitor`, paywall retry/offline/hide-Continue, auth rotator). **0 new issues; still 0 open P0P3.** Live: loader (both themes), splash→handoff, launcher icon, ToT+How Well open (no crash → #4 VM injection sound), paywall purchase screen (friendly error + Try again + Continue hidden, online→generic msg), onboarding illustration. Unit tests green. Gender step / rotator / turn-copy / results-timing / weekly-cap = code+unit-verified (live deferred: fragile multi-text-field & 2-device timing; low risk over proven patterns). Baseline restored (QA re-signed-in via admin token; couple intact).
> **Round 5 (functions deploy + expanded re-QA) COMPLETE 2026-06-25, client `765916a` + functions DEPLOYED:** E-OBS FIXED+DEPLOYED (12 senders set channelId; chat push → `partner_activity` live) + E-003 results-ready FIXED+DEPLOYED (finished-game → per-session results). **0 open P0P3.** New Pass G (account creation + fake-account) clean. Varied gameplay (Standard/Deep, 0-match) + nav fuzzing — no new bugs. Baseline restored (couple intact, throwaway deleted, Sam re-paired). > **Round 5 (functions deploy + expanded re-QA) COMPLETE 2026-06-25, client `765916a` + functions DEPLOYED:** E-OBS FIXED+DEPLOYED (12 senders set channelId; chat push → `partner_activity` live) + E-003 results-ready FIXED+DEPLOYED (finished-game → per-session results). **0 open P0P3.** New Pass G (account creation + fake-account) clean. Varied gameplay (Standard/Deep, 0-match) + nav fuzzing — no new bugs. Baseline restored (couple intact, throwaway deleted, Sam re-paired).
> _Round 4: E-003 + B-004 (P2) + A-OBS (P3) FIXED + verified live._ > _Round 4: E-003 + B-004 (P2) + A-OBS (P3) FIXED + verified live._
@ -62,6 +63,19 @@ _Deferred (nav-drift; standard list/detail, lower-risk): Question Packs detail,
Answer Reveal (sealed), Date Builder/Plan Date, fresh-account auth/onboarding/pairing._ Answer Reveal (sealed), Date Builder/Plan Date, fresh-account auth/onboarding/pairing._
## Pass D — Security & Encryption (D1D6) ## Pass D — Security & Encryption (D1D6)
**R7 DEEP DIVE (multi-angle, 2026-06-25):** **D1 at-rest — CLEAN (admin ground-truth read):** messages `text` +
`lastMessagePreview`, all 4 game-answer collections (`this_or_that`/`how_well`/`desire_sync`/`wheel`, both users),
capsule title+content, `date_swipes.actions` = `enc:v1:`; `wrappedCoupleKey` = ciphertext (recovery-phrase-wrapped,
**argon2id**); `encryptedRecoveryPhrase` server-blind + **wiped on acceptance** (confirmed absent on accepted invite);
plaintext `inviteCode` **not exploitable** (no code-encrypted secret persists; `/invites/{code}` readable only by
inviter). **D3 raw-API negative (LIVE, executed — no longer deferred):** non-member ID token (Identity Toolkit
`signInWithCustomToken`) → Firestore REST on couple doc/conversation/messages/answers/session/capsules/partner-profile
= **all `403 PERMISSION_DENIED`**; non-member writes (couple doc, partner entitlement, **real path
`users/{uid}/entitlements/premium`**) = **all `403` → no self-grant**. Member token reads `200` (characterizes layer:
**App Check not enforced on Firestore — rules are the sole gate, and they hold**). Only writable = cosmetic own-doc
fields (`plan`) that **no gate reads**. **No P0/P1 security findings.** Two hardening notes → `Future.md`.
**R3:** D2 deployed rules re-audited ✅ (B-001 sessions + D-001 capsules/challenges fixes present; hasPremium + **R3:** D2 deployed rules re-audited ✅ (B-001 sessions + D-001 capsules/challenges fixes present; hasPremium +
entitlements server-only; ciphertext enforced; no catch-all). D1 at-rest ✅ (chat text + lastMessagePreview = entitlements server-only; ciphertext enforced; no catch-all). D1 at-rest ✅ (chat text + lastMessagePreview =
`enc:v1:`; how_well answers + capsules = `enc:v1:`). D4/D5/D6 unchanged since R1 (code identical) → hold. `enc:v1:`; how_well answers + capsules = `enc:v1:`). D4/D5/D6 unchanged since R1 (code identical) → hold.
@ -69,6 +83,22 @@ entitlements server-only; ciphertext enforced; no catch-all). D1 at-rest ✅ (ch
statically member-scoped). No P0/P1 security findings. statically member-scoped). No P0/P1 security findings.
## Pass E — Notifications (17 types × {foreground, background, killed} + tap-to-open) ## Pass E — Notifications (17 types × {foreground, background, killed} + tap-to-open)
**R6 live (games + messages, 2026-06-25, build `f47c8e2`):** full live two-device run.
- **chat_message** ✅ end-to-end: Sam→QA (QA bg) posts on **channel=partner_activity**, title "Sam sent a message"
(partner name, not private), body "Tap to read and reply." — **message text NOT in payload** (privacy holds);
small icon = white monochrome mark; tap→**main conversation with content** (verified via the exact intent —
shade-tap is flaky in the adb harness, lands on launcher, but the contentIntent routing is sound).
- **partner_started_game** ✅: QA started This or That → Sam (bg) posts on **channel=game_activity**, "QA is playing /
QA has started a game. Tap to join!" (content-free); tap→**joins the active session** (same 1/5 prompt).
- **partner_finished_game / results** ✅: both finished → **results push DELIVERED to backgrounded QA** (Round 5
couldn't confirm this live) on **channel=game_activity**, "Sam finished the game / Sam finished — tap to see your
results!" (content-free); tap→**per-session This or That results** (5/5), per E-003.
- **#4 results-suppression** ✅: Sam stayed foreground on the session throughout → received **0** notifications
(the partner_completed_part + partner_finished_game pushes to Sam were suppressed by ActiveGameSessionMonitor),
while backgrounded QA got the results push. Clean confirmation of both delivery + suppression.
- No FATAL either device; baseline tidy (0 active sessions, couple intact). **No issues found.**
**R3 live:** FCM tokens valid for both. **chat_message ✅ full chain** (bg deliver + content-free + tap→exact **R3 live:** FCM tokens valid for both. **chat_message ✅ full chain** (bg deliver + content-free + tap→exact
conversation w/ content). **partner_started_game**: bg deliver + content-free ✅; tap→Play hub (not the game) = conversation w/ content). **partner_started_game**: bg deliver + content-free ✅; tap→Play hub (not the game) =
**E-003 (P2)**. **E-OBS (P3)**: bg pushes use fcm_fallback channel. date_match live-verified R2-B2. E-001/E-002 fixes **E-003 (P2)**. **E-OBS (P3)**: bg pushes use fcm_fallback channel. date_match live-verified R2-B2. E-001/E-002 fixes

View File

@ -81,6 +81,30 @@ confirms + enumerates this; the fix phase applies couple-shared everywhere.
- **Test-data hygiene:** keep known test accounts; clean up artifacts (stray messages/reactions/sessions) between - **Test-data hygiene:** keep known test accounts; clean up artifacts (stray messages/reactions/sessions) between
rounds so they don't masquerade as bugs. rounds so they don't masquerade as bugs.
## 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** — force-stop, then deliver + tap a notification; 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) ## Continuity & resumability (this effort WILL span many context windows — don't lose state)
State lives in **files**, not memory: State lives in **files**, not memory:
- **`ClaudeReport.md`** = the issue log (committed). Each issue row is **self-contained in text** (repro + expected - **`ClaudeReport.md`** = the issue log (committed). Each issue row is **self-contained in text** (repro + expected
@ -230,8 +254,12 @@ Account); Paywall; Your Progress/Activity; Recovery.
field, immutability, **no premium self-grant**, entitlements write:false; re-audit conversations/typing/reactions 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 + 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). over-expose; **no legacy plaintext/downgrade path** (`coupleEncryptionEnabled` holds; no disabled-encryption branch).
- **D3 Negative access tests:** a **non-member** account is *denied* reading messages/answers/dates/entitlements, - **D3 Negative access tests (EXECUTE LIVE via raw API — do not defer):** a **non-member** account is *denied* reading
writing plaintext to encrypted fields, self-granting premium, cross-couple access (live rules or rules-emulator). 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** - **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 (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

View File

@ -1,5 +1,24 @@
# Claude QA Report — Full-App QA (living report) # Claude QA Report — Full-App QA (living report)
> **RUN-STATE: Round 7 (multi-angle DEEP DIVE) — 2026-06-25, client HEAD `f47c8e2`, functions deployed. Plan updated with a "Multi-angle attack mandate" + live raw-API D3.** Attacked security/data/concurrency from multiple angles (admin ground-truth read, raw Firestore REST as member+non-member, killed/cold state, malformed intents, simultaneous-start race). **Security cornerstone = FULLY CLEAN (deep):** D1 at-rest — messages/previews + all 4 game-answer collections (ToT/HowWell/DesireSync/Wheel, both users) + capsules + date-swipe actions all `enc:v1:`; couple key phrase-wrapped (argon2id), recovery phrase server-blind + `encryptedRecoveryPhrase` wiped on acceptance, plaintext `inviteCode` not exploitable (invite readable only by inviter; no code-encrypted secret persisted). D3 raw-API — **non-member denied ALL reads/writes (403)**; real premium path `users/{uid}/entitlements/premium` write **denied (403, server-only) → no self-grant**; cross-couple denied. Robustness — malformed/abusive deep-link intents (unknown type, missing extras, injection/path-traversal values) → 0 crash; killed-state cold-start chat deep-link → conversation loads. **NEW FINDING: F-RACE-001 (P1)** — simultaneous game start creates **two divergent active sessions** (TOCTOU). **SEVERITY BOARD: P1 = 1 open (F-RACE-001), P0/P2/P3 = 0 open.** Baseline restored (duplicate sessions ended, 0 active, couple intact). Two hardening notes → Future.md (App Check not enforced on Firestore; user-doc update rule allows arbitrary non-`hasPremium` fields).**
>
> **F-RACE-001 (P1, NEW — concurrency):** When BOTH partners start the *same* game within ~the same second, the couple
> ends up with **2 active sessions with different question sets** (proven live: QA "Which should end the date?" vs Sam
> "Which feels more romantic?", two session docs `bw8Q3X45…` + `yNOzMTOCsGlZPbnv2yBN`). Root cause: `GameSessionManager.
> startGameWithCouple` (usecase/GameSessionManager.kt:84106) does a **non-transactional check-then-create**
> `getActiveSessionForCouple` then `saveSession` (auto-id); two concurrent calls both read null → both create. The
> existing `partner_active_session` guard only covers the non-simultaneous case. Impact: the two partners play separate
> games and never get a shared reveal (core loop silently defeated); two active sessions can also lock/confuse the
> "one active game" rule. No crash, no data loss; recoverable via "End their game"/admin. Repro: stage both at This or
> That mood-select, fire "create" on both in parallel. **Suggested fix:** make session creation atomic — a Firestore
> **transaction** with a per-couple active-session **sentinel** (e.g. `couples/{cid}` field `activeSessionId` or a
> `sessions/_active` doc): read sentinel → if an active session exists, take the join/`partner_active_session` path;
> else create the session doc (client-generated id) + set the sentinel in the same transaction. Clear the sentinel on
> finish. Needs a `firestore.rules` update (member-only sentinel write) + a rules deploy + re-verify all 7 games. (This
> is an architectural change to the core game flow — flagged for a focused fix-phase implementation.)
> **RUN-STATE: Round 6 (branding + Future.md regression QA) COMPLETE — 2026-06-25. Client HEAD `f47c8e2` on both emulators (build == HEAD, reinstalled).** Scope: regression-verify the new surfaces from the branding drop (`95cad84`: white-keyhole launcher/notification icons, animated app-icon-chip loader + fill, cold-launch splash, pairing hero) and the Future.md backlog clear (`f47c8e2`: inclusive gender options, turn-aware Home copy, push rate-limit budget split, results-push suppression via new ActiveGameSessionMonitor, paywall retry/offline/hide-Continue, auth privacy rotator). **0 new issues — SEVERITY BOARD STILL 0 open P0P3.** LIVE-VERIFIED: animated loader (chip+fill, both themes), splash→handoff (white-keyhole icon, no white flash), launcher icon (round mask), This or That + How Well open with no crash (confirms #4's new VM injection is sound), paywall purchase screen shows friendly "Couldn't load plans" + Try again with Continue hidden (no dead button) + online→generic message (#5), onboarding carousel illustration. Unit tests green (NotificationRateLimiter rewritten + PartnerNotificationManagerTest repaired). No FATAL on either device all session. CODE/UNIT-VERIFIED (live deferred, low-risk over proven patterns + fragile multi-text-field/2-device paths): #1 gender step (EditProfile + onboarding sex step — same option list as the shipping Female/Male), #8 rotator on SignUp/Forgot (reuses the Login-proven BrandMessageRotator), #2 "Your turn to play." (static string in the proven GAME_WAITING path), #3 weekly-cap exemption (unit-tested; only triggers at ≥100/wk), #4 results-suppression timing (mechanism + VM wiring verified; simultaneous-finish timing is non-deterministic to drive). Baseline restored: 5554 signed out during the sign-up pass, re-signed-in QA (`Y05AKO2IlTPMa0JQW1BiNIM0uzK2`) via admin custom token; couple `Xal3Kw3gjSdn0niERYKJ` intact, Sam paired.**
> **RUN-STATE: Round 5 (functions deploy + expanded re-QA) COMPLETE — 2026-06-25. Client HEAD `765916a` on both emulators; Cloud Functions DEPLOYED (`firebase deploy --only functions` → "Deploy complete", all 30+ fns updated). Fixed + verified LIVE: E-OBS (all 12 FCM senders now set `android.notification.channelId` → backgrounded chat push lands on `partner_activity`, NOT `fcm_fallback`), E-003 results-ready (server sends `game_session_id`; finished-game deep-link → per-session "This or That Results" screen, not hub/setup). Expanded coverage per user request: VARIED GAMEPLAY (Standard/Deep + 0-match "Total opposites" result path), exhaustive NAV FUZZING (rapid triple-tap opens setup once via launchSingleTop; back-stack clean; no dead-ends/double-back), and NEW PASS G account-creation/fake-account — ALL SECURE: sign-up+validation (weak-pw → friendly error), fresh-account isolation (zero couple data), duplicate-email → `auth/email-already-exists`, invite single-use+24h-expiry + bogus code → "Invite not found", recovery phrase client-generated. **SEVERITY BOARD: 0 open at ALL levels (P0P3).** Baseline restored: couple intact, both free, 0 active sessions, throwaway test account deleted, Sam re-paired.** > **RUN-STATE: Round 5 (functions deploy + expanded re-QA) COMPLETE — 2026-06-25. Client HEAD `765916a` on both emulators; Cloud Functions DEPLOYED (`firebase deploy --only functions` → "Deploy complete", all 30+ fns updated). Fixed + verified LIVE: E-OBS (all 12 FCM senders now set `android.notification.channelId` → backgrounded chat push lands on `partner_activity`, NOT `fcm_fallback`), E-003 results-ready (server sends `game_session_id`; finished-game deep-link → per-session "This or That Results" screen, not hub/setup). Expanded coverage per user request: VARIED GAMEPLAY (Standard/Deep + 0-match "Total opposites" result path), exhaustive NAV FUZZING (rapid triple-tap opens setup once via launchSingleTop; back-stack clean; no dead-ends/double-back), and NEW PASS G account-creation/fake-account — ALL SECURE: sign-up+validation (weak-pw → friendly error), fresh-account isolation (zero couple data), duplicate-email → `auth/email-already-exists`, invite single-use+24h-expiry + bogus code → "Invite not found", recovery phrase client-generated. **SEVERITY BOARD: 0 open at ALL levels (P0P3).** Baseline restored: couple intact, both free, 0 active sessions, throwaway test account deleted, Sam re-paired.**
> _Round 4 (carried): E-003 game-push + B-004 WaitingForPartner "Join the game" + A-OBS paywall copy all FIXED + verified live._ > _Round 4 (carried): E-003 game-push + B-004 WaitingForPartner "Join the game" + A-OBS paywall copy all FIXED + verified live._
> _Round 2 result (carried): FIX PHASE COMPLETE — P1×2 (B-001 session rules, C-NAV-001 back-stack), P2×4 (A-001, B-002, C-CC-001, C-DS-001), P3×4 (A-003, B-003, E-002, F-OBS) all FIXED; D-001 (P1 rules) + E-001 (P2 routing) fixed earlier. All verified LIVE except E-002/F-OBS (code+build; live-trigger deferred). Rules deployed._ > _Round 2 result (carried): FIX PHASE COMPLETE — P1×2 (B-001 session rules, C-NAV-001 back-stack), P2×4 (A-001, B-002, C-CC-001, C-DS-001), P3×4 (A-003, B-003, E-002, F-OBS) all FIXED; D-001 (P1 rules) + E-001 (P2 routing) fixed earlier. All verified LIVE except E-002/F-OBS (code+build; live-trigger deferred). Rules deployed._

View File

@ -14,6 +14,17 @@ Improvement & feature ideas surfaced while QA-testing as a consumer (each works
quiet-hours moon) used consistently would strengthen identity. Generate the G-set, drop the assets in, quiet-hours moon) used consistently would strengthen identity. Generate the G-set, drop the assets in,
then wire them in. *Prompted by:* Pass H branding review. then wire them in. *Prompted by:* Pass H branding review.
### Security hardening (defense-in-depth — not vulnerabilities; rules already hold)
- **Enforce App Check on Firestore (currently OFF).** Round 7 raw-API test: an authenticated request with **no App
Check token** (raw Firestore REST) returned `200` for a member — so rules are the *sole* gate. Rules correctly deny
non-members/cross-couple (all `403`), so this is not a live hole, but enabling App Check enforcement on Firestore
would block non-app clients entirely (defense-in-depth). *Prompted by:* R7 D3 raw-API angle.
- **Tighten the `users/{uid}` update rule to a field allowlist.** The rule only blocks changing `hasPremium`; a user
can write arbitrary *other* fields to their own doc (e.g. a cosmetic `plan`/junk). No gate reads those (premium gates
on the server-only `users/{uid}/entitlements/premium` subcollection + `category.access`), so it grants nothing — but
restricting updates to a known field set is cleaner. *Prompted by:* R7 D3 (`plan` field writable, unused by gating).
> 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.
<!-- <!--

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,230 @@
# Closer Question Quality Checklist v2
**See also:** [QUESTION_CONTENT_GUIDE.md](QUESTION_CONTENT_GUIDE.md) — writing philosophy, voice & tone | [QUESTION_SCHEMA.md](QUESTION_SCHEMA.md) — JSON schema & validation rules | [QUESTION_REWRITE_PLAN.md](QUESTION_REWRITE_PLAN.md) — rewrite checklist & category order
## Purpose
This checklist prevents technically valid but boring questions from reaching the app.
Passing JSON validation is not enough.
Every question must also feel human, useful, and worth answering.
## Automatic Rejects
Reject any question that contains or strongly resembles:
* Describe...
* Reflect on...
* Discuss...
* Evaluate...
* In what ways...
* How satisfied are you...
* What boundary around...
* Explore your feelings...
* Identify the ways...
* Rate the effectiveness...
* Communication style
* Emotional processing
* Conflict framework
* Relationship dynamic
These are therapy worksheet patterns.
Burn them politely.
## Duplicate Pattern Rejects
Reject the question if it repeats too much of a previous question:
* same opening phrase
* same option list
* same emotional goal
* same situation
* same answer pattern
* same wording with one noun swapped
A file can be valid and still feel like a cursed spreadsheet.
## Opening Variety
No category should overuse any opening.
Watch for overuse of:
* What should we...
* What do you...
* Which...
* How much...
* What makes...
These are allowed, but not as the backbone of the pack.
## Consumer Test
Ask:
Would a real couple answer this voluntarily on a Friday night?
Reject if the honest answer is no.
## Conversation Test
A question should create at least one of these:
* a laugh
* a story
* a decision
* a date idea
* a compliment
* a surprise
* a memory
* a useful truth
* a follow up question
Reject questions that only collect data.
## Premium Test
Reject the question if it does not feel worth paying for.
A premium question should feel:
* specific
* warm
* memorable
* useful
* fun
* relationship focused
## Option Quality
For multi_choice and single_choice:
* use 4 to 6 options
* avoid obvious correct answers
* avoid overlapping options
* avoid vague options like "other"
* avoid all negative options
* avoid options that shame either partner
* keep options short
* make options feel realistic
## Written Question Gate
A written question is allowed only when typing creates more value than choices.
Reject written questions that ask for:
* basic preference
* yes or no
* generic reflection
* long emotional labor
* forced vulnerability
Written questions should create stories, memories, or meaningful appreciation.
## Scale Question Gate
Use scale only when intensity matters.
Good scale uses:
* How close does this feel?
* How comfortable are you?
* How often does this happen?
* How important is this?
* How ready are we?
Bad scale uses:
* random preferences
* jokes
* simple choices
* topics better served by options
## Emotional Safety
Reject questions that pressure users to:
* confess
* forgive
* reveal trauma
* admit guilt
* justify boundaries
* defend needs
* disclose sexual details before consent context
* compare partners cruelly
The app should open doors, not corner people.
## Fun Requirement
Every category must include playful moments.
Even serious categories need:
* small humor
* soft wording
* hopeful prompts
* low pressure options
* practical repair ideas
No category should feel like 250 tiny court summons.
## Category Fit
Every question must clearly belong to its category.
Reject questions that could fit anywhere.
Example:
Bad for Gratitude:
* What do you like most about us?
Better:
* What's one small thing I do that deserves more credit?
## Relationship Anchor
Every question should connect to the couple.
Prefer:
* us
* we
* you and me
* our life
* our future
* our memories
Avoid generic survey voice.
## Final Human Read
Before committing a pack, read 30 random questions aloud.
Reject the pack if it sounds:
* robotic
* repetitive
* clinical
* boring
* too similar
* too heavy
* too generic
## Final Approval
A pack is ready only when:
* schema passes
* counts pass
* duplicate checks pass
* tone review passes
* category fit passes
* consumer test passes
* no obvious AI patterns remain

View File

@ -1,126 +1,97 @@
# Closer Question Rewrite Plan # Closer Question Rewrite Plan v2
## Repo Alignment **See also:** [QUESTION_CONTENT_GUIDE.md](QUESTION_CONTENT_GUIDE.md) — writing philosophy, voice & tone | [QUESTION_SCHEMA.md](QUESTION_SCHEMA.md) — JSON schema & validation rules | [QUESTION_QUALITY_CHECKLIST.md](QUESTION_QUALITY_CHECKLIST.md) — quality gate before commit
This plan reflects the current repository schema.
Use these type names exactly:
- written
- single_choice
- multi_choice
- scale
- this_or_that
Do not rename them. Do not introduce `choice` or `either_or` unless the application code is updated first.
## Product Goal ## Product Goal
Closer should feel like a couples game, not a survey or therapy worksheet. Closer is a relationship app, not a survey.
Questions should make couples: The experience should feel like a couples game that naturally creates conversations, laughter, memories, flirting, and meaningful moments.
- smile
- laugh
- learn something new
- remember something
- plan something together
- have meaningful conversations
If a question feels like homework, rewrite it. If a question feels like homework, rewrite it.
## Question Mix ## Repo Schema
Every 250 question pack must contain: Use these type names exactly:
* written
* single_choice
* multi_choice
* scale
* this_or_that
Do not rename them unless the app code is updated first.
## Question Mix per 250
| Type | Count | | Type | Count |
|---|---| |---|---:|
| multi_choice | 140 | | multi_choice | 140 |
| single_choice | 50 | | single_choice | 50 |
| scale | 35 | | scale | 35 |
| this_or_that | 15 | | this_or_that | 15 |
| written | 10 | | written | 10 |
This means 190 of 250 questions, or 76%, are choice based. 76 percent of questions must be choice based.
Written questions should stay rare.
## Free and Premium Split
Per 250 question pack:
* 75 free
* 175 premium
Suggested free split:
* multi_choice: 42
* single_choice: 15
* scale: 11
* this_or_that: 5
* written: 2
Suggested premium split:
* multi_choice: 98
* single_choice: 35
* scale: 24
* this_or_that: 10
* written: 8
Free should feel useful, fun, and complete.
Premium should feel deeper, richer, and more varied.
## Emotional Mix ## Emotional Mix
Aim for: Every category should roughly include:
- 35% fun and playful * 35 percent playful
- 25% everyday relationship * 25 percent everyday relationship
- 20% meaningful conversation * 20 percent meaningful conversation
- 10% future dreams and planning * 10 percent future dreams and planning
- 10% deeper vulnerability * 10 percent deeper vulnerability
Never group heavy questions together. Never group heavy questions together.
Every category must contain lighter moments. Every category must include lighter moments.
## Consumer First Rules
People should naturally answer dozens of questions in one sitting.
Every category should create moments like:
- "I didn't know that."
- "Really?"
- "That's adorable."
- "We should do that."
The product sells shared moments, not data collection.
## Type Definitions
### multi_choice
Select every option that applies.
Default to this type whenever possible.
### single_choice
Select one best answer.
### scale
Rate agreement, comfort, confidence, importance, or frequency.
### this_or_that
Very fast playful questions.
Should take under three seconds.
### written
Reserve for questions where typing creates significantly more value.
Never use written for simple preferences.
## Rewrite Rules ## Rewrite Rules
For every category: For every category:
- Rewrite every question from scratch. * Rewrite every question from scratch.
- Follow the content guide. * Keep category ids.
- Keep category ids. * Use conversational language.
- Keep conversational wording. * Avoid therapy wording.
- Avoid therapy language. * Avoid survey wording.
- Avoid repetitive templates. * Avoid repetitive templates.
- Use 4 to 6 options for choice questions. * Use 4 to 6 options for choice questions.
- Keep written questions rare. * Keep written questions rare.
- Add fun throughout every category. * Add fun to every category.
* Make every category feel different.
## Fun Injection * Reject questions that feel generated.
Every category should include at least:
- 10 playful questions
- 5 questions that create laughter
- 5 questions that inspire a future date, memory, or shared activity
Even serious categories should include enjoyable moments.
## Rewrite Order ## Rewrite Order
@ -146,27 +117,44 @@ Even serious categories should include enjoyable moments.
20. sex_and_desire 20. sex_and_desire
21. sexual_preferences 21. sexual_preferences
## Validation ## Serious Category Rule
Serious categories still need warmth, humor, and relief.
Do not make conflict, trust, boundaries, or intimacy feel like couples counseling homework.
Use soft, safe, specific wording.
## Validation Rules
Every rewritten pack must pass: Every rewritten pack must pass:
- Valid JSON * valid JSON
- Exactly 250 questions * exactly 250 questions
- 75 free * exactly 75 free
- 175 premium * exactly 175 premium
- 140 multi_choice * exactly 140 multi_choice
- 50 single_choice * exactly 50 single_choice
- 35 scale * exactly 35 scale
- 15 this_or_that * exactly 15 this_or_that
- 10 written * exactly 10 written
- Valid depth values only: light, medium, deep * at least 76 percent choice based
- No numeric depth values * valid depth values only: light, medium, deep
- No malformed keys * no numeric depth values
- No duplicate ids * no malformed keys
- No duplicate questions * no duplicate ids
- Every choice question has options * no duplicate question text
- Every scale question has scale configuration * every choice question has options
- Every written question has answer configuration * every scale question has scale configuration
- No therapy worksheet wording * every written question has answer configuration
- No placeholder text * no placeholder text
- Every category feels fun, conversational, and rewarding * no therapy worksheet tone
* no obvious AI repetition
## Final Rule
The schema only proves the file can load.
It does not prove the questions are good.
Quality review is mandatory before commit.

View File

@ -1,5 +1,7 @@
# Closer Question Schema # Closer Question Schema
**See also:** [QUESTION_CONTENT_GUIDE.md](QUESTION_CONTENT_GUIDE.md) — writing philosophy, voice & tone | [QUESTION_REWRITE_PLAN.md](QUESTION_REWRITE_PLAN.md) — rewrite checklist & category order | [QUESTION_QUALITY_CHECKLIST.md](QUESTION_QUALITY_CHECKLIST.md) — quality gate before commit
## Purpose ## Purpose
This document defines the JSON schema, question types, validation rules, and required counts for Closer question packs. This document defines the JSON schema, question types, validation rules, and required counts for Closer question packs.