Closer/seed/questions/QUESTION_SCHEMA.md

11 KiB
Raw Blame History

Closer Question Schema v8 — Importer-Aligned

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

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

category
questions

Required category fields:

id
display_name
description
access
icon_name

metadata is optional but recommended.

Do not use a flat or simple-pack wrapper such as:

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

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:

"depth": 2

Future depth migration — not production-ready

String values are a planned possibility:

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

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:

written
single_choice
multi_choice
scale
this_or_that

single_choice

Use when the player chooses one best answer.

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

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

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

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

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

45 free
105 premium

The count is a ceiling, not a quota.

Daily Single-Choice Weekday System

Compatibility filename:

daily_fun_multiple_choice_v3.json

Production category id:

daily_fun_mc

Logical pack id stored in metadata:

daily_single_choice_weekly_v1

The production file must use the normal top-level wrapper:

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

Required totals:

500 weekday questions
11 wildcard questions
511 total questions
86 free
425 premium
511 single_choice

Every daily question requires:

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:

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.