430 lines
9.9 KiB
Markdown
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.
|