Closer/seed/questions/QUESTION_SCHEMA.md

9.9 KiB

Closer Question Schema v8

See also: QUESTION_CONTENT_GUIDE.md | QUESTION_REWRITE_PLAN.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:

{
  "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:

category
questions

Required category fields:

id
display_name
description
access
icon_name

Optional category fields include:

schema_version
metadata

Never rely on the filename to manufacture a missing category.

The old simple-pack form is not production-valid:

{
  "id": "rebuilding_trust",
  "title": "Rebuilding Trust",
  "count": 25,
  "questions": []
}

Required Question Fields

Every production question requires:

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:

"depth": "light"

Ship:

"depth": 1

The strings light, medium, and deep may appear in coverage maps or review notes only.

Question Types

Use these type names exactly:

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.

{
  "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.

{
  "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.

{
  "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.

{
  "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.

{
  "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:

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:

daily_single_choice_weekly_v1

Compatibility filename:

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:

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:

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.