Closer/seed/questions/QUESTION_SCHEMA.md

430 lines
9.9 KiB
Markdown
Raw Normal View History

2026-07-12 16:02:05 -05:00
# 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
2026-07-12 16:02:05 -05:00
This document defines the production JSON contract used by the active Closer importer and Room database.
2026-07-12 16:02:05 -05:00
The active importer is authoritative. Planning labels and old authoring formats must not override the production contract.
2026-07-12 16:02:05 -05:00
## 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:
2026-07-12 16:02:05 -05:00
```text
written
single_choice
multi_choice
scale
this_or_that
```
2026-07-12 16:02:05 -05:00
Do not rename them unless the app code and importer are updated first.
## multi_choice
2026-07-12 16:02:05 -05:00
Use when more than one answer can be true.
2026-07-12 16:02:05 -05:00
Use 4 to 6 options by default.
```json
{
2026-07-12 16:02:05 -05:00
"id": "quality_time_001",
"category_id": "quality_time",
"type": "multi_choice",
2026-07-12 16:02:05 -05:00
"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" },
2026-07-12 16:02:05 -05:00
{ "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:
2026-07-12 16:02:05 -05:00
- 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
2026-07-12 16:02:05 -05:00
Use when the user selects one best answer.
2026-07-12 16:02:05 -05:00
Use 4 to 6 options by default.
```json
{
2026-07-12 16:02:05 -05:00
"id": "quality_time_002",
"category_id": "quality_time",
"type": "single_choice",
2026-07-12 16:02:05 -05:00
"text": "Which low-key plan sounds best tonight?",
"depth": 1,
"access": "free",
"tags": ["low_energy", "easy_plan", "quality_time"],
"options": [
2026-07-12 16:02:05 -05:00
{ "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:
2026-07-12 16:02:05 -05:00
- 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
2026-07-12 16:02:05 -05:00
Use for one measurable dimension such as comfort, closeness, energy, readiness, satisfaction, confidence, importance, or frequency.
```json
{
2026-07-12 16:02:05 -05:00
"id": "quality_time_003",
"category_id": "quality_time",
"type": "scale",
2026-07-12 16:02:05 -05:00
"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,
2026-07-12 16:02:05 -05:00
"min_label": "Not easy yet",
"max_label": "Very easy"
}
}
```
Rules:
2026-07-12 16:02:05 -05:00
- 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
2026-07-12 16:02:05 -05:00
Use for a fast two-option choice.
```json
{
2026-07-12 16:02:05 -05:00
"id": "quality_time_004",
"category_id": "quality_time",
"type": "this_or_that",
2026-07-12 16:02:05 -05:00
"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:
2026-07-12 16:02:05 -05:00
- exactly two top-level options
- `answer_config.options` must mirror them exactly
- the choice must be quick and balanced
## written
2026-07-12 16:02:05 -05:00
Use only when a short written response adds meaningful value.
```json
{
2026-07-12 16:02:05 -05:00
"id": "quality_time_005",
"category_id": "quality_time",
"type": "written",
2026-07-12 16:02:05 -05:00
"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..."
}
}
```
2026-07-12 16:02:05 -05:00
Rules:
2026-07-12 16:02:05 -05:00
- 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
2026-07-12 12:55:14 -05:00
## Standard Category Packs
2026-07-12 16:02:05 -05:00
A normal category pack may contain up to 150 questions.
2026-07-12 12:55:14 -05:00
2026-07-12 16:02:05 -05:00
A full 150-question mixed pack normally targets:
| Type | Count |
|---|---:|
2026-07-12 12:55:14 -05:00
| multi_choice | 90 |
| single_choice | 30 |
| scale | 15 |
| this_or_that | 10 |
| written | 5 |
2026-07-12 16:02:05 -05:00
Normal full-pack access target:
2026-07-12 12:55:14 -05:00
2026-07-12 16:02:05 -05:00
```text
45 free
105 premium
```
2026-07-12 16:02:05 -05:00
The count is a ceiling, not a quota. Smaller packs are valid when documented and stronger.
2026-07-12 16:02:05 -05:00
## Daily Single-Choice Weekday System
Pack id:
```text
daily_single_choice_weekly_v1
```
2026-07-12 16:02:05 -05:00
Compatibility filename:
```text
daily_fun_multiple_choice_v3.json
```
2026-07-12 16:02:05 -05:00
Required production totals:
| Field | Count |
|---|---:|
2026-07-12 16:02:05 -05:00
| weekday questions | 500 |
| wildcard questions | 11 |
| total questions | 511 |
| free | 86 |
| premium | 425 |
| single_choice | 511 |
2026-07-12 16:02:05 -05:00
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.
2026-07-12 16:02:05 -05:00
Every daily question must include:
2026-07-12 16:02:05 -05:00
- `category_id`
- integer `depth`
- `access`
- `tags`
- top-level `options`
- mirrored `answer_config.options`
2026-07-12 16:02:05 -05:00
Every weekday question must have exactly one weekday tag.
2026-07-12 16:02:05 -05:00
Every wildcard must have `mode_wildcard` and `daily_wildcard` and no weekday tag.
2026-07-12 16:02:05 -05:00
A daily patch manifest is not the daily production pack.
2026-07-12 16:02:05 -05:00
## Work Artifacts Are Not Packs
2026-07-12 16:02:05 -05:00
The following must not be placed under a production filename or inside an importer-scanned production directory:
2026-07-12 16:02:05 -05:00
```text
partial batch JSON
simple-pack batch JSON
patch manifest
marked-fix list
validation report
coverage map
continuation note
apply script
review summary
```
2026-07-12 16:02:05 -05:00
A patch should be clearly named, for example:
2026-07-12 16:02:05 -05:00
```text
daily_fun_multiple_choice_v3.patch.json
```
2026-07-12 16:02:05 -05:00
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.