# Closer Question Schema v8 — Importer-Aligned **See also:** [QUESTION_CONTENT_GUIDE.md](QUESTION_CONTENT_GUIDE.md) | [DAILY_SINGLE_CHOICE_WEEKDAY_SYSTEM.md](DAILY_SINGLE_CHOICE_WEEKDAY_SYSTEM.md) | [QUESTION_REWRITE_PLAN.md](QUESTION_REWRITE_PLAN.md) | [QUESTION_QUALITY_CHECKLIST.md](QUESTION_QUALITY_CHECKLIST.md) ## Authority This document defines the JSON that imports correctly **today**. The active `build_db.py` behavior and the Room storage contract are authoritative for production files. A future schema direction may be documented separately, but it must not appear in production examples until the importer, Room model, migrations, readers, tests, and existing content are updated together. ## Production pack shape Every production pack must use: ```json { "category": { "id": "quality_time", "display_name": "Quality Time", "description": "Questions about being present and enjoying meaningful time together.", "access": "mixed", "icon_name": "schedule", "metadata": {} }, "questions": [] } ``` Required top-level fields: ```text category questions ``` Required category fields: ```text id display_name description access icon_name ``` `metadata` is optional but recommended. Do not use a flat or simple-pack wrapper such as: ```json { "id": "quality_time", "title": "Quality Time", "count": 25, "questions": [] } ``` The importer reads `data["category"]`. A missing category object can cause the file to resolve to `unknown`. Do not rely on the filename to recover category identity. ## Required question fields Every production question requires: ```text id category_id type text depth access tags ``` Rules: - `category_id` must exactly match `category.id` - `type` must be a supported type - `text` must be non-empty and catalog-unique - `depth` must be integer `1`, `2`, or `3` - `access` must be `free` or `premium` - `tags` must be an array - use at least one meaningful tag unless a documented product exception exists - `sex` is optional and nullable for ordinary packs - require `sex` only when a documented feature actually reads it ## Depth: production values | Meaning | Production value | |---|---:| | Light | `1` | | Medium | `2` | | Deep | `3` | Production example: ```json "depth": 2 ``` ### Future depth migration — not production-ready String values are a planned possibility: ```json "depth": "medium" ``` Do **not** ship them today. The current importer writes depth into an integer column, and Room reads the field as an integer. A string can be stored or coerced incorrectly and then read as `0`, breaking depth routing and help text. String depth becomes valid only after a coordinated code and data migration updates: ```text build_db.py Room entity and schema database migration or asset rebuild path all integer depth readers depth routing and help text validation tests all production content ``` Until that migration lands, integers are mandatory. ## Supported question types Use these names exactly: ```text written single_choice multi_choice scale this_or_that ``` ## `single_choice` Use when the player chooses one best answer. ```json { "id": "quality_time_001", "category_id": "quality_time", "type": "single_choice", "text": "Which low-key plan sounds best tonight?", "depth": 1, "access": "free", "tags": ["low_energy", "easy_plan", "quality_time"], "options": [ { "id": "short_walk", "text": "A short walk" }, { "id": "one_episode", "text": "One episode together" }, { "id": "snack_and_talk", "text": "A snack and a talk" }, { "id": "music_on_the_couch", "text": "Music on the couch" } ], "answer_config": { "options": [ { "id": "short_walk", "text": "A short walk" }, { "id": "one_episode", "text": "One episode together" }, { "id": "snack_and_talk", "text": "A snack and a talk" }, { "id": "music_on_the_couch", "text": "Music on the couch" } ] } } ``` Requirements: - use 4 to 6 options by default - top-level `options` are required - `answer_config.options` must mirror top-level options exactly - every option must directly answer the prompt - options should be similar in effort, emotional weight, and intimacy ## `multi_choice` Use when more than one answer can be true. ```json { "id": "quality_time_002", "category_id": "quality_time", "type": "multi_choice", "text": "What helps time together feel easy to enjoy?", "depth": 1, "access": "free", "tags": ["presence", "comfort", "quality_time"], "options": [ { "id": "no_rushing", "text": "Not feeling rushed" }, { "id": "phones_away", "text": "Putting phones away" }, { "id": "easy_conversation", "text": "Easy conversation" }, { "id": "clear_plan", "text": "Knowing the general plan" } ], "answer_config": { "options": [ { "id": "no_rushing", "text": "Not feeling rushed" }, { "id": "phones_away", "text": "Putting phones away" }, { "id": "easy_conversation", "text": "Easy conversation" }, { "id": "clear_plan", "text": "Knowing the general plan" } ], "min_selections": 1, "max_selections": 3 } } ``` Requirements: - use 4 to 6 options by default - top-level `options` are required - `answer_config.options` must mirror them exactly - selection bounds must be valid when included - options must not overlap excessively ## `scale` The importer reads scale settings from `answer_config`. ```json { "id": "quality_time_003", "category_id": "quality_time", "type": "scale", "text": "How easy is it to be present when we finally get time together?", "depth": 2, "access": "free", "tags": ["presence", "attention", "quality_time"], "answer_config": { "min": 1, "max": 5, "min_label": "Not easy yet", "max_label": "Very easy" } } ``` Requirements: - use `answer_config` - include `min`, `max`, `min_label`, and `max_label` - measure one thing only - keep both labels neutral and non-shaming Do not put scale settings in a separate top-level object. The importer does not read custom scale labels from there. ## `written` The importer reads written settings from `answer_config`. ```json { "id": "quality_time_004", "category_id": "quality_time", "type": "written", "text": "What is one recent moment together that felt worth slowing down for?", "depth": 2, "access": "free", "tags": ["recent_memory", "presence", "quality_time"], "answer_config": { "max_length": 500 } } ``` Requirements: - use `answer_config` - include `max_length` when a custom limit matters - do not use a separate top-level `answer` object - use written questions only when typing adds real value - normal packs should contain 0 to 5 written questions ## `this_or_that` A `this_or_that` question must contain its two choices. ```json { "id": "quality_time_005", "category_id": "quality_time", "type": "this_or_that", "text": "Quiet time or playful time?", "depth": 1, "access": "free", "tags": ["quick_choice", "mood", "quality_time"], "options": [ { "id": "quiet_time", "text": "Quiet time" }, { "id": "playful_time", "text": "Playful time" } ], "answer_config": { "options": [ { "id": "quiet_time", "text": "Quiet time" }, { "id": "playful_time", "text": "Playful time" } ] } } ``` Requirements: - exactly two top-level options - `answer_config.options` must mirror them exactly - never ship a bare prompt with no A/B choices - both choices should be balanced ## Standard category packs A normal pack may contain up to 150 questions. A full 150-question target is: | Type | Count | |---|---:| | `multi_choice` | 90 | | `single_choice` | 30 | | `scale` | 15 | | `this_or_that` | 10 | | `written` | 5 | Normal full-pack access target: ```text 45 free 105 premium ``` The count is a ceiling, not a quota. ## Daily Single-Choice Weekday System Compatibility filename: ```text daily_fun_multiple_choice_v3.json ``` Production category id: ```text daily_fun_mc ``` Logical pack id stored in metadata: ```text daily_single_choice_weekly_v1 ``` The production file must use the normal top-level wrapper: ```json { "category": { "id": "daily_fun_mc", "display_name": "Daily Fun", "description": "One quick couples-game question for each day, including wildcard surprise days.", "access": "mixed", "icon_name": "calendar_today", "metadata": { "pack_id": "daily_single_choice_weekly_v1", "question_type_policy": "single_choice_only", "total_questions": 511, "free_questions": 86, "premium_questions": 425 } }, "questions": [] } ``` When editing the existing production file, preserve its current category display fields and icon unless the product owner explicitly changes them. The important contract is that the category object exists and its `id` matches every question’s `category_id`. Required totals: ```text 500 weekday questions 11 wildcard questions 511 total questions 86 free 425 premium 511 single_choice ``` Every daily question requires: ```text category_id: daily_fun_mc integer depth access tags top-level options mirrored answer_config.options ``` Every weekday question must carry exactly one documented weekday tag. Every wildcard must carry `mode_wildcard` and `daily_wildcard`, and must not carry a weekday tag. A patch manifest containing only changed IDs is not the Daily production pack. ## Work artifacts are not production packs Do not place any of these under a production filename: ```text partial batch JSON simple-pack batch JSON patch manifest marked-fix list validation report coverage map continuation note apply script review summary ``` A patch must be clearly named as a patch, applied to the complete source, and followed by validation of the complete resulting pack. ## Per-file validation Before shipping: - JSON parses - top-level `category` exists - top-level `questions` exists - all required category fields exist - every required question field exists - every `category_id` matches `category.id` - every `depth` is integer `1`, `2`, or `3` - all IDs are unique inside the file - all question text is unique inside the file - all access values are valid - every tags value is an array - choice option IDs are unique per question - mirrored option arrays match exactly - scale and written settings are in `answer_config` - every `this_or_that` has exactly two choices - metadata counts match actual counts - the file is complete, not a batch or patch ## Catalog-wide validation Before rebuilding `app.db` or shipping content, validate every production pack together. Reject the build for: - duplicate question IDs across files - duplicate exact question text across files - duplicate normalized question text after case and whitespace normalization - blocked near-duplicates across categories - any category resolving to `unknown` - any production filename containing a work artifact - any schema mismatch that can coerce a value or silently discard configuration A duplicate in two otherwise valid files can abort the entire import. Per-file success does not override catalog failure.