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
> 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 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._
## 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 +
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.
@ -69,6 +83,22 @@ entitlements server-only; ciphertext enforced; no catch-all). D1 at-rest ✅ (ch
statically member-scoped). No P0/P1 security findings.
## 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
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

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
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)
State lives in **files**, not memory:
- **`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
+ 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:** a **non-member** account is *denied* reading messages/answers/dates/entitlements,
writing plaintext to encrypted fields, self-granting premium, cross-couple access (live rules or rules-emulator).
- **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

View File

@ -1,5 +1,24 @@
# 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.**
> _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._

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,
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.
<!--

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
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.
**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
## 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:
- smile
- laugh
- learn something new
- remember something
- plan something together
- have meaningful conversations
The experience should feel like a couples game that naturally creates conversations, laughter, memories, flirting, and meaningful moments.
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 |
|---|---|
|---|---:|
| multi_choice | 140 |
| single_choice | 50 |
| scale | 35 |
| this_or_that | 15 |
| 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
Aim for:
Every category should roughly include:
- 35% fun and playful
- 25% everyday relationship
- 20% meaningful conversation
- 10% future dreams and planning
- 10% deeper vulnerability
* 35 percent playful
* 25 percent everyday relationship
* 20 percent meaningful conversation
* 10 percent future dreams and planning
* 10 percent deeper vulnerability
Never group heavy questions together.
Every category must contain 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.
Every category must include lighter moments.
## Rewrite Rules
For every category:
- Rewrite every question from scratch.
- Follow the content guide.
- Keep category ids.
- Keep conversational wording.
- Avoid therapy language.
- Avoid repetitive templates.
- Use 4 to 6 options for choice questions.
- Keep written questions rare.
- Add fun throughout every category.
## Fun Injection
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 every question from scratch.
* Keep category ids.
* Use conversational language.
* Avoid therapy wording.
* Avoid survey wording.
* Avoid repetitive templates.
* Use 4 to 6 options for choice questions.
* Keep written questions rare.
* Add fun to every category.
* Make every category feel different.
* Reject questions that feel generated.
## Rewrite Order
@ -146,27 +117,44 @@ Even serious categories should include enjoyable moments.
20. sex_and_desire
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:
- Valid JSON
- Exactly 250 questions
- 75 free
- 175 premium
- 140 multi_choice
- 50 single_choice
- 35 scale
- 15 this_or_that
- 10 written
- Valid depth values only: light, medium, deep
- No numeric depth values
- No malformed keys
- No duplicate ids
- No duplicate questions
- Every choice question has options
- Every scale question has scale configuration
- Every written question has answer configuration
- No therapy worksheet wording
- No placeholder text
- Every category feels fun, conversational, and rewarding
* valid JSON
* exactly 250 questions
* exactly 75 free
* exactly 175 premium
* exactly 140 multi_choice
* exactly 50 single_choice
* exactly 35 scale
* exactly 15 this_or_that
* exactly 10 written
* at least 76 percent choice based
* valid depth values only: light, medium, deep
* no numeric depth values
* no malformed keys
* no duplicate ids
* no duplicate question text
* every choice question has options
* every scale question has scale configuration
* every written question has answer configuration
* no placeholder text
* 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
**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
This document defines the JSON schema, question types, validation rules, and required counts for Closer question packs.