# Closer Question Schema v8 **See also:** [QUESTION_CONTENT_GUIDE.md](QUESTION_CONTENT_GUIDE.md) | [QUESTION_REWRITE_PLAN.md](QUESTION_REWRITE_PLAN.md) | [QUESTION_QUALITY_CHECKLIST.md](QUESTION_QUALITY_CHECKLIST.md) ## Purpose This document defines the production JSON contract used by the active Closer importer and Room database. The active importer is authoritative. Planning labels and old authoring formats must not override the production contract. ## Production File Shape Every production question pack must use this top-level shape: ```json { "category": { "id": "quality_time", "display_name": "Quality Time", "description": "Questions about being present and enjoying meaningful time together.", "access": "mixed", "icon_name": "schedule", "schema_version": "question_v2", "metadata": {} }, "questions": [] } ``` Required top-level keys: ```text category questions ``` Required `category` fields: ```text id display_name description access icon_name ``` Optional category fields include: ```text schema_version metadata ``` Never rely on the filename to manufacture a missing category. The old simple-pack form is not production-valid: ```json { "id": "rebuilding_trust", "title": "Rebuilding Trust", "count": 25, "questions": [] } ``` ## Required Question Fields Every production question requires: ```text id category_id type text depth access tags ``` Rules: - `category_id` must exactly match the enclosing `category.id` - `type` must be one of the supported type names - `text` must be non-empty and catalog-unique - `depth` must be the 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 exception exists - `sex` is optional and nullable for ordinary packs - require `sex` only when a documented feature, such as feature-specific targeting, actually reads it ## Depth Production depth is numeric: | Meaning | Value | |---|---:| | Light | `1` | | Medium | `2` | | Deep | `3` | Do not ship: ```json "depth": "light" ``` Ship: ```json "depth": 1 ``` The strings `light`, `medium`, and `deep` may appear in coverage maps or review notes only. ## Question Types Use these type names exactly: ```text written single_choice multi_choice scale this_or_that ``` Do not rename them unless the app code and importer are updated first. ## multi_choice Use when more than one answer can be true. Use 4 to 6 options by default. ```json { "id": "quality_time_001", "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 } } ``` Rules: - top-level `options` are required - `answer_config.options` must mirror the top-level options exactly - selection bounds, when present, must be valid for the number of options - options must directly and grammatically answer the prompt - options must not overlap excessively ## single_choice Use when the user selects one best answer. Use 4 to 6 options by default. ```json { "id": "quality_time_002", "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" } ] } } ``` Rules: - top-level `options` are required - `answer_config.options` must mirror them exactly - every option must be a plausible single best answer - options should be similar in effort, emotional weight, and intimacy ## scale Use for one measurable dimension such as comfort, closeness, energy, readiness, satisfaction, confidence, importance, or frequency. ```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" } } ``` Rules: - use `answer_config`, not a top-level `scale` object - `min`, `max`, `min_label`, and `max_label` are required - labels must be gentle and neutral - measure one thing only ## this_or_that Use for a fast two-option choice. ```json { "id": "quality_time_004", "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" } ] } } ``` Rules: - exactly two top-level options - `answer_config.options` must mirror them exactly - the choice must be quick and balanced ## written Use only when a short written response adds meaningful value. ```json { "id": "quality_time_005", "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": { "min_length": 1, "max_length": 500, "placeholder": "Write your answer..." } } ``` Rules: - use `answer_config`, not a top-level `answer` object - `max_length` is required - do not use written questions for basic preferences - normal packs should contain 0 to 5 written questions ## Standard Category Packs A normal category pack may contain up to 150 questions. A full 150-question mixed pack normally targets: | 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. Smaller packs are valid when documented and stronger. ## Daily Single-Choice Weekday System Pack id: ```text daily_single_choice_weekly_v1 ``` Compatibility filename: ```text daily_fun_multiple_choice_v3.json ``` Required production totals: | Field | Count | |---|---:| | weekday questions | 500 | | wildcard questions | 11 | | total questions | 511 | | free | 86 | | premium | 425 | | single_choice | 511 | The compatibility filename does not change the schema. The file must still be a complete production pack with a required `category` object and `questions` array. Every daily question must include: - `category_id` - integer `depth` - `access` - `tags` - top-level `options` - mirrored `answer_config.options` Every weekday question must have exactly one weekday tag. Every wildcard must have `mode_wildcard` and `daily_wildcard` and no weekday tag. A daily patch manifest is not the daily production pack. ## Work Artifacts Are Not Packs The following must not be placed under a production filename or inside an importer-scanned production directory: ```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 should be clearly named, for example: ```text daily_fun_multiple_choice_v3.patch.json ``` It must be applied to the complete production source, and the complete resulting pack must pass validation before shipping. ## Per-File Validation Requirements Before shipping a pack: - JSON parses - top-level `category` object exists - top-level `questions` array exists - required category fields exist - every question has every required field - 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 types are valid - tags arrays exist - access values are valid - option IDs are unique within each question - top-level options and `answer_config.options` match where required - metadata counts match actual counts - normal packs contain no more than 150 questions - normal packs contain no more than 5 written questions unless an approved exception exists - special packs match their exact documented contract - the file is a complete pack, not a patch or partial batch ## Catalog-Wide Validation Requirements Before rebuilding `app.db` or shipping question content, validate all production files together. The full catalog must have: - globally unique question IDs - globally unique exact question text - no duplicate text after case and whitespace normalization - no blocked near-duplicates across categories - no category resolving to `unknown` - no production file containing a work artifact - no field coercion caused by a schema mismatch A duplicate question in two different files is a hard failure and may abort the entire import. Per-file success does not override a catalog-wide failure.