{
 "$schema": "https://json-schema.org/draft/2020-12/schema",
 "$id": "furnigen-brief/0",
 "title": "Бриф мебели v0 (контракт слоя 1 → слоя 2)",
 "description": "Факты о людях и помещении, пожелания клиента и согласованные решения. Не манифест furnigen: модулей и зазоров здесь нет. Схема проверяет форму; полноту и нормы проверяет brief_check.py. Каждое число — объект qty с источником (src); число, намеренное клиентом, помечено needs_measure (\"потребує заміру\"). Предмет разговора задаёт subject: от него зависит, о чём спрашивать — о плите и вытяжке, о том, что вешают в шкаф, или о том, что кладут в ящики. Брифы без subject читаются как кухонные, какими они и были.",
 "type": "object",
 "additionalProperties": false,
 "required": [
  "format",
  "id",
  "language",
  "status",
  "motives",
  "household",
  "room",
  "appliances",
  "habits",
  "style",
  "decisions",
  "conflicts",
  "open_questions",
  "wishes_verbatim"
 ],
 "properties": {
  "format": {
   "const": "furnigen-brief/0"
  },
  "subject": {
   "type": "array",
   "description": "что проектируем. kitchen — кухня; wardrobe — шкаф для одежды (высокий, со штангой, полками и антресолью); storage — мебель для хранения не одежды (инструменты, документы, посуда, материалы), а также комод, тумба и стеллаж — даже с одеждой: комод не шкаф, и эскиз шкафа на всю стену с его ценой ему не подходит (жалоба 30.09); у storage и other проектировщика нет — считает человек после заявки; other — что-то ещё, и тогда обязателен subject_note. Отсутствует — кухня: так бриф читался до того, как появились шкафы, и старые брифы этого не заметили. Не то же, что room.kind: шкаф может стоять и в кухне-гостиной, а мебель для хранения — в кабинете. bathroom — мебель ванной: тумба под умывальник, пенал, зеркальный шкаф (ROADMAP Р15). До ворот В4 слой 2 отдаёт bathroom в схеме и принимает только с флагом владельца FURNIGEN_BATHROOM=1; без флага помощник его не видит и живым людям не предлагает",
   "minItems": 1,
   "items": {
    "enum": [
     "kitchen",
     "wardrobe",
     "storage",
     "other",
     "bathroom"
    ]
   }
  },
  "subject_note": {
   "type": "string",
   "minLength": 1,
   "description": "что это за предмет, словами клиента: «меблі для зберігання інструментів», «стіл для майстерні». Обязателен при subject: other, уместен везде, где слово из перечисления описывает предмет грубо"
  },
  "id": {
   "type": "string",
   "pattern": "^[a-z0-9][a-z0-9_-]{2,63}$"
  },
  "language": {
   "const": "uk"
  },
  "status": {
   "description": "draft — пишет помощник; ready — только по ответу POST /brief/check",
   "enum": [
    "draft",
    "ready"
   ]
  },
  "meta": {
   "type": "object",
   "additionalProperties": false,
   "properties": {
    "created": {
     "type": "string"
    },
    "updated": {
     "type": "string"
    },
    "author": {
     "type": "string",
     "description": "кто писал бриф: помощник (модель) или человек"
    },
    "participants": {
     "type": "array",
     "description": "кто говорил в разговоре (группа Telegram — вся семья); пишет бот, а не модель. Только имя: идентификаторы мессенджера в бриф не попадают",
     "items": {
      "$ref": "#/$defs/participant"
     }
    },
    "confirmations": {
     "type": "array",
     "description": "кто нажал «Підтверджую» и под каким отпечатком брифа (ROADMAP 1.G4); пишет бот. Заказ группы принимается, только когда подтвердил каждый, кто решает, и бриф с тех пор не менялся",
     "items": {
      "type": "object",
      "additionalProperties": false,
      "required": [
       "id",
       "fingerprint"
      ],
      "properties": {
       "id": {
        "$ref": "#/$defs/participant_ref"
       },
       "fingerprint": {
        "type": "string",
        "pattern": "^[0-9a-f]{16}$"
       }
      }
     }
    },
    "note": {
     "type": "string"
    },
    "channel": {
     "enum": [
      "telegram",
      "cabinet",
      "ai_handoff",
      "ai_mcp",
      "ai_http",
      "site"
     ],
     "description": "откуда пришёл бриф: бот (telegram), кабинет, ИИ заказчика ссылкой или текстом (ai_handoff), через наш MCP-сервер (ai_mcp) или HTTP API (ai_http), конфигуратор на сайте furnigen.app (site). Пишет наш код, а не модель. Нет поля — бриф до 02.10, считать telegram"
    },
    "agent": {
     "type": "string",
     "description": "какой ИИ заказчика вёл разговор (chatgpt, claude, gemini, openclaw…), как он сам назвался или как его опознал наш сервер; только для учёта канала, не для решений",
     "minLength": 1
    },
    "from_ai_memory": {
     "type": "array",
     "description": "поля, которые ИИ заказчика заполнил по памяти о человеке, а не со слов в разговоре (ограничения, рука, привычки, техника…). Числа так не помечаются: у них src: ai_memory. Человек видит этот список на странице подтверждения и может убрать любой пункт",
     "items": {
      "type": "object",
      "additionalProperties": false,
      "required": [
       "path"
      ],
      "properties": {
       "path": {
        "type": "string",
        "minLength": 1,
        "description": "путь поля, как его пишет brief_check в missing (household.people[p1].limits)"
       },
       "note": {
        "type": "string",
        "description": "что именно ИИ знал и откуда, коротко"
       }
      }
     }
    }
   }
  },
  "motives": {
   "type": "object",
   "additionalProperties": false,
   "properties": {
    "primary": {
     "$ref": "#/$defs/motive"
    },
    "ranking": {
     "type": "array",
     "items": {
      "$ref": "#/$defs/motive"
     }
    },
    "budget_min": {
     "$ref": "#/$defs/qty"
    },
    "budget_max": {
     "$ref": "#/$defs/qty"
    },
    "said": {
     "type": "array",
     "items": {
      "type": "string",
      "minLength": 1
     },
     "description": "слова клиента о мотиве, дословно"
    }
   }
  },
  "household": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "people"
   ],
   "properties": {
    "people": {
     "type": "array",
     "items": {
      "$ref": "#/$defs/person"
     }
    },
    "pets": {
     "type": "array",
     "items": {
      "type": "string"
     }
    },
    "guests": {
     "type": "string",
     "description": "как часто и сколько гостей, словами клиента"
    }
   }
  },
  "room": {
   "type": "object",
   "additionalProperties": false,
   "properties": {
    "kind": {
     "enum": [
      "separate_kitchen",
      "kitchen_living",
      "kitchen_niche",
      "bedroom",
      "hallway",
      "dressing_room",
      "office",
      "niche",
      "other"
     ]
    },
    "building": {
     "enum": [
      "apartment",
      "house",
      "other"
     ]
    },
    "construction": {
     "enum": [
      "existing",
      "new",
      "unknown"
     ],
     "description": "существующий дом (ремонт) или новое строительство: для газа в существующих домах есть послабления ДБН В.2.5-20 п. 9.29"
    },
    "ceiling_height": {
     "$ref": "#/$defs/qty"
    },
    "floor_area": {
     "$ref": "#/$defs/qty"
    },
    "shape_hint": {
     "enum": [
      "linear",
      "l_shape",
      "u_shape",
      "parallel",
      "island",
      "peninsula",
      "undecided"
     ],
     "description": "форма, о которой говорил клиент; окончательно выбирает проектировщик"
    },
    "walls": {
     "type": "array",
     "items": {
      "$ref": "#/$defs/wall"
     }
    },
    "openings": {
     "type": "array",
     "items": {
      "$ref": "#/$defs/opening"
     },
     "description": "окна, двери, арки, ниши; пустой список — клиент подтвердил, что их нет на стенах под мебель"
    },
    "services": {
     "type": "object",
     "additionalProperties": false,
     "properties": {
      "water": {
       "$ref": "#/$defs/service_point"
      },
      "drain": {
       "$ref": "#/$defs/service_point"
      },
      "gas": {
       "$ref": "#/$defs/service_point"
      },
      "vent_duct": {
       "$ref": "#/$defs/service_point"
      },
      "sockets_note": {
       "type": "string"
      },
      "points": {
       "type": "array",
       "items": {
        "$ref": "#/$defs/service_mark"
       },
       "description": "точки по видам SERVICE_KINDS, в том числе розетки и пожелания клиента — качественно, без чисел (2.P4)"
      }
     }
    },
    "photos": {
     "type": "array",
     "items": {
      "type": "string"
     }
    },
    "plan_files": {
     "type": "array",
     "items": {
      "type": "string"
     }
    }
   }
  },
  "unknown_until_measure": {
   "type": "array",
   "items": {
    "$ref": "#/$defs/deferred"
   },
   "description": "что клиент не знает и что выяснит замер: проёмы, выводы, длины стен, высота потолка. Такой бриф считается полным, а перечисленное уходит в needs_measure"
  },
  "appliances": {
   "type": "array",
   "items": {
    "$ref": "#/$defs/appliance"
   }
  },
  "storage": {
   "type": "object",
   "additionalProperties": false,
   "description": "что храним и кто этим пользуется. Виды одежды (hanging_long … suitcases) — для subject: wardrobe; stored словами клиента — для subject: storage, где одежды нет вовсе. Для кухни блок не нужен: её хранение описано habits.storage",
   "properties": {
    "users": {
     "type": "array",
     "description": "кто пользуется этим шкафом — ссылки на household.people. От их роста зависит, докуда ставить штангу и верхнюю полку",
     "items": {
      "$ref": "#/$defs/person_ref"
     }
    },
    "stored": {
     "type": "array",
     "description": "что кладут — словами клиента: «інструменти та витратні матеріали», «документи й папір», «посуд». Так описывает своё хранение мебель, в которой нет одежды: перечислять ей виды плечиков нечего",
     "items": {
      "type": "string",
      "minLength": 1
     }
    },
    "hanging_long": {
     "$ref": "#/$defs/amount",
     "description": "длинное на плечиках: пальто, платья, шубы"
    },
    "hanging_short": {
     "$ref": "#/$defs/amount",
     "description": "короткое на плечиках: рубашки, пиджаки, юбки"
    },
    "folded": {
     "$ref": "#/$defs/amount",
     "description": "сложенное на полках: свитера, футболки, бельё"
    },
    "shoes": {
     "$ref": "#/$defs/amount"
    },
    "bedding": {
     "$ref": "#/$defs/amount",
     "description": "постельное, подушки, одеяла"
    },
    "seasonal": {
     "$ref": "#/$defs/amount",
     "description": "то, что достают раз в полгода"
    },
    "suitcases": {
     "$ref": "#/$defs/amount"
    },
    "depth_max": {
     "$ref": "#/$defs/qty",
     "properties": {
      "unit": {
       "const": "mm"
      }
     },
     "description": "предельная глубина шкафа, которую назвал клиент («не глибше 45 см»: проход, двери напротив) — в мм, слова клиента в said. Не поле «для порядка»: если под плечики нужно глубже (gost13025.wardrobe.hanging_depth), это конфликт с нормой и 2–3 варианта (торцевая выдвижная штанга, полки вместо плечиков)"
    },
    "doors": {
     "description": "распашные, купе, только ящики, ящики вместе с распашными дверцами (drawers_and_doors: «поєднати шухляди з дверцятами» — комод, тумба) или открытые секции. undecided — клиент не выбрал, и это ещё не ответ: от типа зависит вся конструкция",
     "enum": [
      "swing",
      "sliding",
      "drawers",
      "drawers_and_doors",
      "open",
      "undecided"
     ]
    },
    "locks": {
     "description": "нужны ли замки. Вопрос мебели, которой пользуются многие (инструменты в общем кабинете), а не шкафа в спальне",
     "enum": [
      "yes",
      "no",
      "undecided"
     ]
    },
    "mirror": {
     "enum": [
      "none",
      "outside",
      "inside",
      "undecided"
     ]
    },
    "notes": {
     "type": "string"
    }
   }
  },
  "bathroom": {
   "type": "object",
   "additionalProperties": false,
   "description": "Мебель ванной (subject: bathroom, ROADMAP Р15). Всё необязательное: бриф без блока валиден. Высоту умывальника не спрашивают — её задаёт норма (dbn64.bath.basin_height: 800 ± 20 мм, 850 при общем смесителе с ванной); спрашивают то, от чего зависит конструкция тумбы. Как и subject: bathroom, блок есть в схеме только с флагом FURNIGEN_BATHROOM",
   "properties": {
    "vanity": {
     "enum": [
      "floor",
      "wall_hung",
      "undecided"
     ],
     "description": "тумба на опорах или подвесная"
    },
    "basin": {
     "enum": [
      "vessel",
      "inset",
      "furniture",
      "undecided"
     ],
     "description": "умывальник: накладная чаша на столешнице, врезной в столешницу или мебельный (лежит на корпусе)"
    },
    "basins": {
     "type": "integer",
     "minimum": 1,
     "maximum": 2,
     "description": "сколько умывальников; два — оси не ближе 650 мм (dbn64.bath.basin_spacing)"
    },
    "mixer": {
     "enum": [
      "deck",
      "wall",
      "shared_with_bath",
      "undecided"
     ],
     "description": "смеситель на умывальнике или столешнице, из стены или один на ванну и умывальник (тогда борт 850 мм)"
    },
    "storage": {
     "enum": [
      "doors",
      "drawers",
      "undecided"
     ],
     "description": "под умывальником дверцы или ящики (ящики обходят сифон вырезом)"
    },
    "tall": {
     "type": "boolean",
     "description": "пенал рядом"
    },
    "mirror_cabinet": {
     "type": "boolean",
     "description": "зеркальный шкаф над умывальником"
    },
    "said": {
     "type": "string",
     "minLength": 1,
     "description": "что клиент сказал о ванной своими словами"
    }
   }
  },
  "habits": {
   "type": "object",
   "additionalProperties": false,
   "description": "кухонные привычки: готовка, посуда, где едят, что делают при отключениях света (outage) и сколько посуды и техники хранят (inventory). Заполняется только когда subject содержит kitchen; у шкафа и мебели для хранения раздел остаётся пустым",
   "properties": {
    "dishwasher": {
     "type": "boolean",
     "description": "нужна ли посудомойка — обязательный ответ"
    },
    "cooking": {
     "enum": [
      "daily_full",
      "daily_simple",
      "few_times_week",
      "rarely"
     ]
    },
    "cooks_together": {
     "type": "boolean",
     "description": "готовят ли двое одновременно"
    },
    "storage": {
     "type": "string",
     "description": "запасы, крупная посуда — словами клиента"
    },
    "eating_in_kitchen": {
     "enum": [
      "table",
      "bar",
      "none",
      "undecided"
     ]
    },
    "notes": {
     "type": "array",
     "items": {
      "type": "string"
     }
    },
    "outage": {
     "$ref": "#/$defs/outage"
    },
    "inventory": {
     "$ref": "#/$defs/inventory"
    }
   }
  },
  "style": {
   "type": "object",
   "additionalProperties": false,
   "properties": {
    "words": {
     "type": "array",
     "items": {
      "type": "string"
     }
    },
    "references": {
     "type": "array",
     "items": {
      "type": "string"
     },
     "description": "фото-примеры, ссылки"
    },
    "fronts": {
     "description": "выбор фасадів: \"UI\" — выбирает сам клиент на 3D-странице; либо код из GET /options (перечень подставляет каталог при выдаче схемы); либо настоящий декор поставщика — код VY<артикул> из GET /decors (показан клиенту фото, 1.V3); либо его собственные слова — объектом {\"said\": \"…\"}, чтобы их не приняли за код каталога",
     "anyOf": [
      {
       "type": "string",
       "minLength": 1
      },
      {
       "type": "string",
       "pattern": "^VY[0-9]+$"
      },
      {
       "type": "object",
       "additionalProperties": false,
       "required": [
        "said"
       ],
       "properties": {
        "said": {
         "type": "string",
         "minLength": 1
        }
       }
      }
     ]
    },
    "worktop": {
     "description": "выбор стільниці: \"UI\" — выбирает сам клиент на 3D-странице; либо код из GET /options (перечень подставляет каталог при выдаче схемы); либо настоящий декор поставщика — код VY<артикул> из GET /decors (показан клиенту фото, 1.V3); либо его собственные слова — объектом {\"said\": \"…\"}, чтобы их не приняли за код каталога",
     "anyOf": [
      {
       "type": "string",
       "minLength": 1
      },
      {
       "type": "string",
       "pattern": "^VY[0-9]+$"
      },
      {
       "type": "object",
       "additionalProperties": false,
       "required": [
        "said"
       ],
       "properties": {
        "said": {
         "type": "string",
         "minLength": 1
        }
       }
      }
     ]
    },
    "handles": {
     "description": "выбор ручок: \"UI\" — выбирает сам клиент на 3D-странице; либо код из GET /options (перечень подставляет каталог при выдаче схемы); либо его собственные слова — объектом {\"said\": \"…\"}, чтобы их не приняли за код каталога",
     "anyOf": [
      {
       "type": "string",
       "minLength": 1
      },
      {
       "type": "object",
       "additionalProperties": false,
       "required": [
        "said"
       ],
       "properties": {
        "said": {
         "type": "string",
         "minLength": 1
        }
       }
      }
     ]
    }
   }
  },
  "hardware": {
   "type": "object",
   "additionalProperties": false,
   "description": "фурнитура, выбранная клиентом: в slides/hinges/lifts/slide_systems — только коды каталога. Система, которую клиент назвал, а в каталоге её нет, пишется в requested — словами клиента и, если серия известна (GET /options?named= → known), её id",
   "properties": {
    "slides": {
     "type": "string",
     "minLength": 1,
     "description": "код направляющих или ящичной системы из GET /options раздел hardware; список допустимых значений подставляет каталог при выдаче схемы"
    },
    "hinges": {
     "type": "string",
     "minLength": 1,
     "description": "код петель из GET /options раздел hardware; список допустимых значений подставляет каталог при выдаче схемы"
    },
    "lifts": {
     "type": "string",
     "minLength": 1,
     "description": "код подъёмника из GET /options раздел hardware; список допустимых значений подставляет каталог при выдаче схемы"
    },
    "slide_systems": {
     "type": "string",
     "minLength": 1,
     "description": "код раздвижной системы шкафа-купе из GET /options раздел hardware; список допустимых значений подставляет каталог при выдаче схемы"
    },
    "requested": {
     "type": "array",
     "description": "системы, которые клиент хочет, а в расчёт они ещё не заведены: проект считается на замене (known → stand_in), а заявка ставит серию в очередь на заведение",
     "items": {
      "type": "object",
      "additionalProperties": false,
      "required": [
       "group",
       "said"
      ],
      "properties": {
       "group": {
        "enum": [
         "slides",
         "hinges",
         "lifts",
         "slide_systems"
        ]
       },
       "said": {
        "type": "string",
        "minLength": 1,
        "description": "как назвал клиент, дословно («Тандембокс», «ящики як у мами»)"
       },
       "known": {
        "type": "string",
        "minLength": 1,
        "description": "id известной серии из GET /options?named= (known[].id); список подставляется из реестра при выдаче схемы. Серия незнакомая — поля нет"
       }
      }
     }
    }
   }
  },
  "audit": {
   "type": "object",
   "additionalProperties": false,
   "description": "аудит нынешней кухни (ROADMAP Р11): клиент не заказывает новую кухню, а просит оценить ту, что стоит, — что неудобно и что поправить без новой (органайзеры, свет, высота). Есть блок — разговор про аудит, отчёт строит POST /brief/audit по правилам базы. Размеры здесь — нынешней мебели, а не помещения: замерщику они не идут. Для кухни, которую проектируем, блок не нужен",
   "properties": {
    "said": {
     "type": "string",
     "minLength": 1,
     "description": "просьба словами клиента: «оцініть мою кухню», «що тут можна покращити»"
    },
    "complaints": {
     "type": "array",
     "description": "что мешает сейчас — словами клиента; about — к чему это относится, если ясно",
     "items": {
      "type": "object",
      "additionalProperties": false,
      "required": [
       "said"
      ],
      "properties": {
       "said": {
        "type": "string",
        "minLength": 1
       },
       "about": {
        "enum": [
         "back",
         "reach_high",
         "bending",
         "light",
         "storage",
         "worktop_space",
         "hood",
         "other"
        ]
       },
       "said_by": {
        "$ref": "#/$defs/participant_ref"
       }
      }
     }
    },
    "worktop_height": {
     "$ref": "#/$defs/qty",
     "description": "нынешняя столешница: от пола до верхнего края"
    },
    "hood_above_hob": {
     "$ref": "#/$defs/qty",
     "description": "от варочной поверхности до низа вытяжки"
    },
    "wall_bottom": {
     "$ref": "#/$defs/qty",
     "description": "низ навесных шкафов от пола"
    },
    "base_storage": {
     "enum": [
      "shelves",
      "drawers",
      "mixed",
      "unknown"
     ],
     "description": "что в нижних шкафах: полки за дверцами, выдвижные ящики или и то, и то"
    },
    "task_light": {
     "enum": [
      "none",
      "ceiling_only",
      "under_cabinet",
      "unknown"
     ],
     "description": "свет над рабочей поверхностью: нет, только потолочный, подсветка под навесными"
    },
    "seen": {
     "type": "array",
     "description": "что видно на фото кухни (пишет бот после чтения фото, не модель в разговоре). Только перечисленные слова — чисел с фото не бывает (ROADMAP Р13)",
     "items": {
      "type": "object",
      "additionalProperties": false,
      "required": [
       "what",
       "value"
      ],
      "properties": {
       "what": {
        "enum": [
         "task_light",
         "base_storage",
         "hood",
         "wall_cabinets"
        ]
       },
       "value": {
        "enum": [
         "none",
         "ceiling_only",
         "under_cabinet",
         "shelves",
         "drawers",
         "mixed",
         "present",
         "absent"
        ]
       },
       "photo": {
        "type": "string",
        "description": "имя файла фото"
       }
      }
     }
    }
   }
  },
  "decisions": {
   "type": "array",
   "items": {
    "$ref": "#/$defs/decision"
   }
  },
  "conflicts": {
   "type": "array",
   "items": {
    "$ref": "#/$defs/conflict"
   }
  },
  "open_questions": {
   "type": "array",
   "items": {
    "type": "string",
    "minLength": 1
   }
  },
  "wishes_verbatim": {
   "type": "array",
   "items": {
    "type": "string",
    "minLength": 1
   }
  }
 },
 "$defs": {
  "motive": {
   "description": "economy — зекономити; ergonomics — зручність і здоров'я; prestige — красиво й дорого; undecided — клієнт не визначився: законна відповідь, якщо в motives.said його слова (тоді ranking не пишуть). Вибір за клієнта не робиться",
   "enum": [
    "economy",
    "ergonomics",
    "prestige",
    "undecided"
   ]
  },
  "src": {
   "description": "откуда число: client_said — сказал (не размер помещения: любой размер помещения от клиента — client_measure; размеры нынешних шкафов в habits.inventory — client_said); client_measure — намерил сам (потребує заміру); plan — с плана/проекта; photo — оценено по фото; site_measure — замер специалиста; default — подставлено по правилу базы (rule_id обязателен); derived — вычислено из других чисел брифа (note обязателен); ai_memory — со слов ИИ, которым пользуется сам заказчик (ChatGPT, Claude, Gemini…), из его памяти о человеке, а не сказано в этом разговоре. Человек видит такие факты отдельно на странице подтверждения и может убрать (AGENT_CHANNEL_RECON_0210 §4). Размер помещения с ai_memory — как client_measure: needs_measure: true",
   "enum": [
    "client_said",
    "client_measure",
    "plan",
    "photo",
    "site_measure",
    "default",
    "derived",
    "ai_memory"
   ]
  },
  "qty": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "v",
    "unit",
    "src"
   ],
   "properties": {
    "v": {
     "type": "number"
    },
    "unit": {
     "enum": [
      "mm",
      "m2",
      "m3",
      "kW",
      "count",
      "uah",
      "years",
      "deg"
     ]
    },
    "src": {
     "$ref": "#/$defs/src"
    },
    "needs_measure": {
     "type": "boolean"
    },
    "rule_id": {
     "type": "string"
    },
    "said": {
     "type": "string",
     "description": "как сказал клиент, если отличается от v (\"158 см\", \"десь 3 метри\")"
    },
    "said_by": {
     "$ref": "#/$defs/participant_ref"
    },
    "private": {
     "type": "boolean",
     "description": "сказано наедине (бюджет в личке, ROADMAP 1.G5): в группе число не называть"
    },
    "note": {
     "type": "string"
    }
   },
   "allOf": [
    {
     "if": {
      "properties": {
       "src": {
        "enum": [
         "client_measure",
         "photo"
        ]
       }
      },
      "required": [
       "src"
      ]
     },
     "then": {
      "required": [
       "needs_measure"
      ]
     }
    },
    {
     "if": {
      "properties": {
       "src": {
        "const": "client_measure"
       }
      },
      "required": [
       "src"
      ]
     },
     "then": {
      "properties": {
       "needs_measure": {
        "const": true
       }
      }
     }
    },
    {
     "if": {
      "properties": {
       "src": {
        "const": "ai_memory"
       }
      },
      "required": [
       "src"
      ]
     },
     "then": {
      "required": [
       "needs_measure"
      ]
     }
    },
    {
     "if": {
      "properties": {
       "src": {
        "const": "default"
       }
      },
      "required": [
       "src"
      ]
     },
     "then": {
      "required": [
       "rule_id"
      ]
     }
    },
    {
     "if": {
      "properties": {
       "src": {
        "const": "derived"
       }
      },
      "required": [
       "src"
      ]
     },
     "then": {
      "required": [
       "note"
      ]
     }
    }
   ]
  },
  "person": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "id",
    "role"
   ],
   "properties": {
    "id": {
     "type": "string",
     "pattern": "^p[0-9]+$"
    },
    "role": {
     "type": "string",
     "description": "как клиент назвал: я, чоловік, мама, син"
    },
    "main_cook": {
     "type": "boolean",
     "description": "только когда subject содержит kitchen: у мебели для хранения повара нет"
    },
    "cooks": {
     "enum": [
      "daily",
      "often",
      "sometimes",
      "rarely",
      "never"
     ],
     "description": "только когда subject содержит kitchen"
    },
    "age": {
     "$ref": "#/$defs/qty"
    },
    "age_group": {
     "enum": [
      "child",
      "teen",
      "adult",
      "senior"
     ]
    },
    "height": {
     "$ref": "#/$defs/qty"
    },
    "elbow_height": {
     "$ref": "#/$defs/qty",
     "description": "высота локтя стоя от пола, если клиент её померил: точнее оценки по росту. От неё считают рабочую высоту (amk.work_height.below_elbow)"
    },
    "handedness": {
     "enum": [
      "right",
      "left",
      "both",
      "unknown"
     ]
    },
    "limits": {
     "type": "array",
     "description": "ограничения; пустой список — спросили, ограничений нет; отсутствие поля — не спрашивали",
     "items": {
      "$ref": "#/$defs/limit"
     }
    },
    "limits_note": {
     "type": "string"
    },
    "said_by": {
     "$ref": "#/$defs/participant_ref"
    }
   }
  },
  "limit": {
   "enum": [
    "back_pain",
    "knee_pain",
    "hand_arthritis",
    "wheelchair",
    "walker_or_cane",
    "low_vision",
    "hard_bending",
    "hard_reaching_up",
    "prosthesis",
    "pregnancy",
    "other"
   ]
  },
  "wall": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "id",
    "length"
   ],
   "properties": {
    "id": {
     "type": "string",
     "pattern": "^w[0-9]+$"
    },
    "name": {
     "type": "string",
     "description": "как клиент называет стену: біля вікна, де мийка"
    },
    "length": {
     "$ref": "#/$defs/qty"
    },
    "furnish": {
     "type": "boolean",
     "description": "ставить ли мебель вдоль этой стены"
    },
    "next_wall": {
     "type": "string",
     "description": "id стены по часовой стрелке от правого конца"
    },
    "corner_angle": {
     "$ref": "#/$defs/qty",
     "properties": {
      "unit": {
       "const": "deg"
      }
     },
     "description": "внутренний угол у правого конца стены, между ней и next_wall, в градусах (unit: deg): прямой — 90"
    },
    "note": {
     "type": "string"
    }
   }
  },
  "opening": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "id",
    "kind",
    "wall"
   ],
   "properties": {
    "id": {
     "type": "string",
     "pattern": "^o[0-9]+$"
    },
    "kind": {
     "enum": [
      "window",
      "door",
      "balcony_door",
      "arch",
      "niche",
      "radiator",
      "pipe_riser",
      "other"
     ]
    },
    "wall": {
     "type": "string"
    },
    "offset": {
     "$ref": "#/$defs/qty"
    },
    "width": {
     "$ref": "#/$defs/qty"
    },
    "height": {
     "$ref": "#/$defs/qty"
    },
    "sill": {
     "$ref": "#/$defs/qty"
    },
    "depth": {
     "$ref": "#/$defs/qty"
    },
    "note": {
     "type": "string"
    }
   }
  },
  "service_point": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "present"
   ],
   "properties": {
    "present": {
     "type": "boolean"
    },
    "wall": {
     "type": "string"
    },
    "offset": {
     "$ref": "#/$defs/qty"
    },
    "height": {
     "$ref": "#/$defs/qty"
    },
    "movable": {
     "enum": [
      "yes",
      "no",
      "unknown"
     ]
    },
    "note": {
     "type": "string"
    },
    "src": {
     "$ref": "#/$defs/src"
    },
    "needs_measure": {
     "type": "boolean"
    }
   },
   "description": "вывод воды, канализации, газа или вентканала. src и needs_measure — если сведение не с замера: с фото — src: photo и всегда needs_measure: true (2.P4); без src — как раньше",
   "allOf": [
    {
     "if": {
      "properties": {
       "src": {
        "enum": [
         "client_measure",
         "photo"
        ]
       }
      },
      "required": [
       "src"
      ]
     },
     "then": {
      "required": [
       "needs_measure"
      ],
      "properties": {
       "needs_measure": {
        "const": true
       }
      }
     }
    }
   ]
  },
  "appliance": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "kind"
   ],
   "properties": {
    "kind": {
     "enum": [
      "fridge",
      "freezer",
      "hob",
      "oven",
      "microwave",
      "hood",
      "dishwasher",
      "washer",
      "coffee_machine",
      "wine_cooler",
      "sink",
      "power_station",
      "other"
     ],
     "description": "power_station — резервное питание на случай отключений (ROADMAP Р8): портативная зарядная станция или инвертор с аккумулятором — клиенту «резервне живлення», не «зарядна станція» (отзыв тестера 03.10: на зарядных станциях не готовят, а инвертор с батареей зовут иначе). model — как назвал клиент, built_in: yes — стоит в нише мебели, no — отдельно; ниша, розетка и вентиляция — по паспорту модели (правила ecoflow.station.* — про портативную станцию; для инвертора с аккумулятором правил в базе нет, docs/KB_GAPS_LAYER1.md)"
    },
    "fuel": {
     "enum": [
      "gas",
      "electric",
      "induction",
      "gas_electric"
     ],
     "description": "для hob и oven. gas_electric — комбинированная варочная: газовые и электрические конфорки в одной панели или пара доміно (газ + электро/индукция). По ДБН В.2.5-20, п. 3.13 это газоиспользующее оборудование: к кухне — все требования газовой плиты (dbn20.term.gas_using_equipment), а электрической части — розетка или линия по паспорту модели"
    },
    "burners": {
     "$ref": "#/$defs/qty"
    },
    "power": {
     "$ref": "#/$defs/qty"
    },
    "built_in": {
     "enum": [
      "yes",
      "no",
      "undecided"
     ]
    },
    "status": {
     "enum": [
      "have",
      "buying",
      "undecided",
      "not_needed"
     ]
    },
    "hood_type": {
     "enum": [
      "built_in",
      "wall",
      "inclined",
      "island",
      "ceiling",
      "worktop",
      "none",
      "undecided"
     ]
    },
    "hood_mode": {
     "enum": [
      "duct",
      "recirculation",
      "undecided"
     ]
    },
    "width": {
     "$ref": "#/$defs/qty"
    },
    "height": {
     "$ref": "#/$defs/qty"
    },
    "depth": {
     "$ref": "#/$defs/qty"
    },
    "model": {
     "type": "string"
    },
    "placement_wish": {
     "type": "string",
     "description": "где клиент хочет поставить, дословно"
    },
    "placement": {
     "enum": [
      "in_run",
      "separate",
      "undecided"
     ],
     "description": "где стоит техника (пока — для fridge): in_run — в ряду мебели (встроенный в пенал или отдельностоящий в ряду); separate — стоит отдельно, вне мебели (у двери, в нише стены, в другом месте): проектировщик не ставит его в ряд и ниши не считает, а треугольник «холодильник — мойка — плита» проверяется на замере; undecided — не решили. Нет поля — как раньше: в ряду, если слова placement_wish не говорят «окремо»"
    },
    "wall": {
     "type": "string",
     "pattern": "^w[0-9]+$",
     "description": "id стены (room.walls[].id), у которой клиент хочет эту технику: для in_run — стена ряда, для separate — где стоит"
    },
    "wall_end": {
     "enum": [
      "start",
      "end"
     ],
     "description": "у какого края стены wall: start — у начала (откуда меряют offset проёмов и выводов), end — у конца. Проектировщик ставит холодильник туда, если ряд от этого не теряет ни техники, ни норм, иначе говорит в журнале, почему нет"
    }
   }
  },
  "decision": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "id",
    "topic",
    "rule_ids",
    "value",
    "by",
    "explained"
   ],
   "properties": {
    "id": {
     "type": "string",
     "pattern": "^d[0-9]+$"
    },
    "topic": {
     "type": "string",
     "description": "параметр: worktop_height, oven_position, lower_row, handles_kind, ..."
    },
    "rule_ids": {
     "type": "array",
     "items": {
      "type": "string"
     },
     "description": "правила базы, на которые опирается решение; пусто — решение вкуса без правила"
    },
    "value": {
     "description": "число — только объект qty с источником; иначе перечисление или короткий текст",
     "anyOf": [
      {
       "$ref": "#/$defs/qty"
      },
      {
       "type": "string",
       "minLength": 1
      },
      {
       "type": "boolean"
      }
     ]
    },
    "by": {
     "enum": [
      "client",
      "assistant_recommended",
      "rule_default"
     ]
    },
    "client_accepted": {
     "type": "boolean"
    },
    "explained": {
     "type": "boolean",
     "description": "клиенту объяснили, почему так"
    },
    "for_person": {
     "type": "string"
    },
    "said_by": {
     "$ref": "#/$defs/participant_ref"
    },
    "note": {
     "type": "string"
    }
   }
  },
  "conflict": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "id",
    "wish_verbatim",
    "rule_ids",
    "severity",
    "options_offered",
    "status"
   ],
   "properties": {
    "id": {
     "type": "string",
     "pattern": "^c[0-9]+$"
    },
    "kind": {
     "enum": [
      "rule",
      "people"
     ],
     "description": "rule (по умолчанию) — желание против правила базы; people — участники группы хотят разного (ROADMAP 1.G3)"
    },
    "sides": {
     "type": "array",
     "minItems": 2,
     "description": "для kind: people — кто чего хочет, словами каждого",
     "items": {
      "type": "object",
      "additionalProperties": false,
      "required": [
       "said_by",
       "wish"
      ],
      "properties": {
       "said_by": {
        "$ref": "#/$defs/participant_ref"
       },
       "wish": {
        "type": "string",
        "minLength": 1
       }
      }
     }
    },
    "wish_verbatim": {
     "type": "string",
     "minLength": 1
    },
    "rule_ids": {
     "type": "array",
     "items": {
      "type": "string"
     },
     "description": "правила базы; у kind: people может быть пусто — спор вкуса"
    },
    "severity": {
     "enum": [
      "norm",
      "recommendation",
      "taste"
     ]
    },
    "options_offered": {
     "type": "array",
     "items": {
      "type": "string",
      "minLength": 1
     }
    },
    "status": {
     "enum": [
      "open",
      "resolved"
     ]
    },
    "resolution": {
     "type": "string"
    },
    "keeps_wish": {
     "type": "boolean",
     "description": "клиент остался при своём (допустимо только для recommendation и taste)"
    },
    "decision_id": {
     "type": "string"
    },
    "said_by": {
     "$ref": "#/$defs/participant_ref"
    }
   },
   "if": {
    "properties": {
     "kind": {
      "const": "people"
     }
    },
    "required": [
     "kind"
    ]
   },
   "then": {
    "required": [
     "sides"
    ]
   },
   "else": {
    "properties": {
     "rule_ids": {
      "minItems": 1
     }
    }
   }
  },
  "participant": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "id",
    "name"
   ],
   "properties": {
    "id": {
     "type": "string",
     "pattern": "^u[0-9]+$"
    },
    "name": {
     "type": "string",
     "minLength": 1,
     "description": "имя, как оно видно в разговоре"
    },
    "person_id": {
     "type": "string",
     "pattern": "^p[0-9]+$",
     "description": "кто это из жильцов (household.people) — только если человек сам сказал; не угадывать"
    },
    "decides": {
     "type": "boolean",
     "description": "решает ли за кухню: его «Підтверджую» нужно для заказа (по умолчанию да; false — например, ребёнок или гость чата)"
    }
   }
  },
  "person_ref": {
   "type": "string",
   "pattern": "^p[0-9]+$",
   "description": "кто из домашних: id из household.people"
  },
  "amount": {
   "type": "object",
   "additionalProperties": false,
   "description": "сколько этого добра. Клиент говорит словами («небагато», «дуже багато»), метры появляются, если он их назвал или посчитали по вещам — и тогда у числа есть источник, как у всех",
   "required": [
    "level"
   ],
   "properties": {
    "level": {
     "enum": [
      "none",
      "little",
      "average",
      "lots"
     ]
    },
    "metres": {
     "$ref": "#/$defs/qty"
    },
    "said": {
     "type": "string"
    }
   }
  },
  "outage": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "plan"
   ],
   "description": "кухня на случай отключений света (ROADMAP Р8): чем готовят и что держат включённым, когда света нет. Только масштаб кухни: ни солнечных панелей, ни схемы щита. Выбрано резервное питание (зарядная станция или инвертор с аккумулятором) — оно же отдельной техникой appliances kind: power_station (модель, в нише или отдельно)",
   "properties": {
    "plan": {
     "type": "array",
     "minItems": 1,
     "description": "gas — газовая плита (готовит без света); power_station — резервное питание: портативная зарядная станция или инвертор с аккумулятором (клиенту — «резервне живлення», код прежний ради совместимости); generator — генератор вне кухни; none — ничего не планируют; undecided — не решили (тогда обязательны слова клиента в said)",
     "items": {
      "enum": [
       "gas",
       "power_station",
       "generator",
       "none",
       "undecided"
      ]
     }
    },
    "powers": {
     "type": "array",
     "description": "что хотят питать от резервного питания во время отключения — от этого зависит, какие вилки должны дотянуться до его ниши",
     "items": {
      "enum": [
       "hob",
       "fridge",
       "kettle",
       "microwave",
       "lights",
       "router",
       "other"
      ]
     }
    },
    "said": {
     "type": "string",
     "minLength": 1,
     "description": "слова клиента об отключениях, дословно"
    },
    "said_by": {
     "$ref": "#/$defs/participant_ref"
    }
   }
  },
  "inventory": {
   "type": "object",
   "additionalProperties": false,
   "description": "инвентаризация хранения кухни (ROADMAP Р12): что у семьи есть из посуды и техники и сколько места это занимает сейчас. Из этого расчёт выводит нужную длину полок и ящиков (формула NKBA: ширина × число уровней × глубина в футах) вместо значения по умолчанию по площади кухни. Числа — только сказанные или намеренные клиентом, с источником; сам итог считает алгоритм, а не помощник",
   "properties": {
    "items": {
     "type": "array",
     "description": "что есть — словами клиента; количество, только если он его назвал",
     "items": {
      "$ref": "#/$defs/inventory_item"
     }
    },
    "now": {
     "type": "array",
     "description": "где это лежит сейчас: нынешние шкафы, полки и ящики. Каждый вид — одной строкой: ширина, сколько таких, сколько в каждом уровней (полки вместе с дном; у ящика — 1), глубина, если назвали",
     "items": {
      "$ref": "#/$defs/storage_unit"
     }
    },
    "fit": {
     "enum": [
      "enough",
      "short",
      "spare",
      "no_kitchen"
     ],
     "description": "влезает ли всё сейчас: enough — впору; short — не влезает, стоит на столешнице, на балконе; spare — есть пустое место; no_kitchen — своей кухни сейчас нет, сравнивать не с чем"
    },
    "said": {
     "type": "string",
     "minLength": 1,
     "description": "слова клиента о том, как всё помещается сейчас"
    },
    "said_by": {
     "$ref": "#/$defs/participant_ref"
    }
   }
  },
  "inventory_item": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "kind",
    "said"
   ],
   "properties": {
    "kind": {
     "enum": [
      "cookware",
      "tableware",
      "glassware",
      "cutlery",
      "small_appliances",
      "food_stock",
      "bakeware",
      "containers",
      "cleaning",
      "textiles",
      "other"
     ],
     "description": "cookware — кастрюли и сковороды; tableware — тарелки и чашки; glassware — бокалы; cutlery — приборы; small_appliances — мелкая техника (мультиварка, блендер, тостер); food_stock — запасы продуктов; bakeware — формы для выпечки; containers — контейнеры и банки; cleaning — бытовая химия; textiles — полотенца и скатерти"
    },
    "said": {
     "type": "string",
     "minLength": 1,
     "description": "что именно, словами клиента"
    },
    "amount": {
     "enum": [
      "little",
      "average",
      "lots"
     ]
    },
    "count": {
     "$ref": "#/$defs/qty"
    },
    "on_worktop": {
     "type": "boolean",
     "description": "для мелкой техники: стоит на столешнице постоянно, а не убирается в шкаф"
    },
    "said_by": {
     "$ref": "#/$defs/participant_ref"
    }
   }
  },
  "storage_unit": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "kind",
    "width"
   ],
   "description": "один вид нынешнего хранения: «три нижні шафки по 60 з полицею», «чотири шухляди по 40». Размеры здесь — не размеры помещения: src client_said, замерщик их не перепроверяет и в «потребує заміру» они не идут",
   "properties": {
    "kind": {
     "enum": [
      "base",
      "wall",
      "tall",
      "drawer",
      "open_shelf",
      "other"
     ]
    },
    "width": {
     "$ref": "#/$defs/qty"
    },
    "count": {
     "$ref": "#/$defs/qty",
     "description": "сколько таких; нет поля — один"
    },
    "levels": {
     "$ref": "#/$defs/qty",
     "description": "сколько уровней в каждом: полки вместе с дном шкафа; у ящика — 1. Нет поля у шкафа — длину посчитать нельзя, строка не войдёт в сумму"
    },
    "depth": {
     "$ref": "#/$defs/qty"
    },
    "said": {
     "type": "string"
    }
   }
  },
  "participant_ref": {
   "type": "string",
   "pattern": "^u[0-9]+$",
   "description": "кто это сказал: id участника из meta.participants (в группе, где говорят несколько человек)"
  },
  "deferred": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "path",
    "said"
   ],
   "description": "клиент не знает этого сам, и выяснит замер. Не «мы не спросили»: в said лежат слова клиента, которыми он ответил «не знаю»",
   "properties": {
    "path": {
     "type": "string",
     "description": "поле брифа, как его называет brief_check в missing"
    },
    "said": {
     "type": "string",
     "minLength": 1,
     "description": "слова клиента: чем он ответил на вопрос"
    },
    "said_by": {
     "$ref": "#/$defs/participant_ref"
    }
   }
  },
  "service_mark": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "kind",
    "where",
    "src"
   ],
   "description": "точка инженерки качественно, без чисел (2.P4): что и где примерно — «праворуч від вікна, приблизно на висоті пояса». Координаты вывода даёт замер, поэтому ни x, ни высоты в мм здесь нет",
   "properties": {
    "kind": {
     "enum": [
      "socket",
      "power",
      "switch",
      "water_cold",
      "water_hot",
      "drain",
      "vent_exhaust",
      "vent_supply",
      "duct",
      "gas"
     ],
     "description": "вид по SERVICE_KINDS ядра (model.py)"
    },
    "state": {
     "enum": [
      "existing",
      "wanted",
      "unclear"
     ],
     "description": "existing — есть на месте; wanted — пометка клиента «хочу тут»; unclear — не понять, к точке есть вопрос"
    },
    "where": {
     "type": "string",
     "minLength": 1,
     "description": "где — словами, относительно окна, угла, мойки; без размеров"
    },
    "wall": {
     "type": "string",
     "description": "id стены брифа, если стена известна"
    },
    "level": {
     "enum": [
      "floor",
      "low",
      "waist",
      "high",
      "ceiling",
      "unknown"
     ],
     "description": "высота качественно: у пола, ниже столешницы, на высоте пояса, над столешницей, под потолком"
    },
    "count": {
     "type": "integer",
     "minimum": 1,
     "description": "постов в одной рамке (блок розеток)"
    },
    "src": {
     "$ref": "#/$defs/src"
    },
    "needs_measure": {
     "type": "boolean"
    },
    "photo": {
     "type": "string",
     "description": "принятый снимок: имя-отпечаток в хранилище листов (2.P1)"
    },
    "note": {
     "type": "string"
    }
   },
   "allOf": [
    {
     "if": {
      "properties": {
       "src": {
        "enum": [
         "client_measure",
         "photo"
        ]
       }
      },
      "required": [
       "src"
      ]
     },
     "then": {
      "required": [
       "needs_measure"
      ],
      "properties": {
       "needs_measure": {
        "const": true
       }
      }
     }
    }
   ]
  }
 }
}
