Closer/seed/questions/QUESTION_SCHEMA.md

430 lines
9.9 KiB
Markdown

# 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.