openapi: 3.1.0
info:
  title: Jarvis API
  version: 1.0.0
  description: |
    Jarvis — личный ассистент Кирилла Пукшто (Section 228).
    10 микросервисов на VPS 95.179.180.154, общий контракт через ServiceBase
    (POST /service + GET /health) + главный роутер jarvis-main и miniapp_api
    backend для Telegram Mini App.

    ## Топология
    - **jarvis-main** (port 8765) — главный роутер: `/tasks/next`, `/callback`,
      `/notify`, `/heartbeat` (для конкретной задачи, `/tasks/heartbeat`),
      `/settings`, `/context`, `/registry`. ВНИМАНИЕ: у jarvis-main НЕТ
      `GET /health` (он не построен на ServiceBase). Health-индикатором
      выступает `GET /tasks/next` (200 OK с пустым `{}` если очередь пуста).
    - **miniapp_api** (port 8780) — backend для Mini App: `/service` + actions,
      `/heartbeat`, `/heartbeat-status`, `/health`. Проксируется через nginx на
      https://erema-pkd.duckdns.org/api
    - **10 микросервисов** (8771–8782) — astro, digest, deck, wiki, finance,
      city, weather, reminder, dispatcher, decisions

    ## Контракт ServiceBase
    Все микросервисы (кроме jarvis-main) построены на одном базовом классе
    `service_base.py` и принимают `POST /service` с body `{task_id, payload, context?}`,
    возвращают `{task_id, status, result, duration_ms, error?, next_service?}`.
    `GET /health` отдаёт `{ok, service, uptime_s, pid, port, ...extra}`.

  contact:
    name: Kiryl Pukshto
    email: k.pukshto@section228.com

servers:
  - url: https://erema-pkd.duckdns.org/api
    description: Production miniapp_api через nginx (port 8780)
  - url: http://95.179.180.154:8765
    description: jarvis-main (главный роутер, callback от Mac)
  - url: http://127.0.0.1:8771
    description: astro service (внутренний)
  - url: http://127.0.0.1:8772
    description: digest service (внутренний)
  - url: http://127.0.0.1:8773
    description: deck service (внутренний)
  - url: http://127.0.0.1:8775
    description: wiki service (внутренний)
  - url: http://127.0.0.1:8776
    description: finance service (внутренний)
  - url: http://127.0.0.1:8777
    description: city service (внутренний)
  - url: http://127.0.0.1:8778
    description: weather service (внутренний)
  - url: http://127.0.0.1:8779
    description: reminder service (внутренний)
  - url: http://127.0.0.1:8780
    description: miniapp_api (внутренний)
  - url: http://127.0.0.1:8781
    description: dispatcher service (внутренний)
  - url: http://127.0.0.1:8782
    description: decisions service (внутренний)

tags:
  - name: miniapp
    description: miniapp_api endpoints — backend для Telegram Mini App
  - name: main
    description: jarvis-main роутер — оркестрация Mac/VPS, очередь задач
  - name: astro
    description: Натальные карты и синастрии (kerykeion + Swiss Ephemeris)
  - name: digest
    description: Сводка чатов с динамическим scoring значимости
  - name: deck
    description: Discovery презентаций Section 228
  - name: wiki
    description: Поиск по локальному зеркалу s228-wiki
  - name: finance
    description: P&L / DevModel CSV preview
  - name: city
    description: Гайды городов (city-*.md)
  - name: weather
    description: Погода через Open-Meteo
  - name: reminder
    description: Напоминания (create/list/dismiss/snooze)
  - name: dispatcher
    description: Оркестратор сложных задач (Mac первичен, Sonnet fallback)
  - name: decisions
    description: Жёсткая очередь архитектурных/продуктовых решений (CRUD + stats)
  - name: health
    description: GET /health для каждого сервиса (кроме jarvis-main)

paths:
  # ────────────────────────────────────────────────────────────────────────
  # miniapp_api
  # ────────────────────────────────────────────────────────────────────────
  /service:
    post:
      tags: [miniapp]
      summary: Универсальный action endpoint miniapp_api
      description: |
        Единая точка входа для Mini App. Маршрутизация по `payload.action`.
        В отличие от других микросервисов, здесь payload содержит обязательный
        ключ `action` — он определяет какой именно метод бэкенда вызывается.
      operationId: miniappService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MiniAppServiceRequest'
            examples:
              status:
                summary: status — health всех сервисов
                value:
                  task_id: "ui-1"
                  payload: {action: "status"}
              tasks_active:
                summary: tasks_active — активные задачи
                value:
                  task_id: "ui-2"
                  payload: {action: "tasks_active"}
              history:
                summary: history — bot_messages за N дней
                value:
                  task_id: "ui-3"
                  payload: {action: "history", days: 7}
              services_log:
                summary: services_log — tail лога сервиса
                value:
                  task_id: "ui-4"
                  payload: {action: "services_log", service: "astro", lines: 100}
              agents_active:
                summary: agents_active — активные агенты + дерево подзадач
                value:
                  task_id: "ui-5"
                  payload: {action: "agents_active"}
              limits:
                summary: limits — стоимость API за периоды
                value:
                  task_id: "ui-6"
                  payload: {action: "limits"}
      responses:
        '200':
          description: Результат action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MiniAppServiceResponse'
        '400':
          description: Невалидный JSON или формат payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Ошибка обработки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-actions:
        status:
          description: |
            Health всех 10 микросервисов (astro/digest/deck/wiki/finance/city/
            weather/reminder/dispatcher/decisions) + main systemd info + pool
            (Mac heartbeat) + counts (messages/tasks/reminders).
          result: '#/components/schemas/StatusResult'
        tasks_active:
          description: |
            До 100 задач из task_queue со статусом pending|processing.
            Включает processing_for_sec — сколько задача висит в работе.
          result: '#/components/schemas/TasksActiveResult'
        history:
          description: |
            До 1000 строк из bot_messages за последние N дней (1..30, default 1).
            Поля: id, direction (in|out), text, created_at.
          params:
            days:
              type: integer
              description: Сколько дней назад (1..30)
              default: 1
          result: '#/components/schemas/HistoryResult'
        services_log:
          description: |
            tail -n <lines> логов сервиса из /var/log/jarvis-services/<name>.log.
            Whitelist по SERVICES (10 микросервисов: astro, digest, deck, wiki,
            finance, city, weather, reminder, dispatcher, decisions).
          params:
            service:
              type: string
              enum: [astro, digest, deck, wiki, finance, city, weather, reminder, dispatcher, decisions]
            lines:
              type: integer
              description: Количество строк (1..1000, default 50)
              default: 50
          result: '#/components/schemas/ServicesLogResult'
        agents_active:
          description: |
            Корневые running/processing/pending задачи task_queue (parent_id IS NULL,
            с непустым agent_name) + их подзадачи. До 50 корней.
          result: '#/components/schemas/AgentsActiveResult'
        limits:
          description: |
            Использование API за today/week/month + by_model (за 30 дней).
            Источник — таблица api_usage (input_tokens/output_tokens/cost_usd/model).
          result: '#/components/schemas/LimitsResult'

  /heartbeat:
    post:
      tags: [miniapp]
      summary: Heartbeat от Mac (миник)
      description: |
        Mac пингует каждые ~10 секунд. Body содержит снапшот пула воркеров.
        miniapp_api запоминает last_heartbeat_ts для определения mac_online.
      operationId: miniappHeartbeat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MacHeartbeatRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MacHeartbeatResponse'

  /heartbeat-status:
    get:
      tags: [miniapp]
      summary: Текущее состояние heartbeat (отладка)
      description: |
        Возвращает текущий снапшот пула + mac_online + last_heartbeat_age_s.
        Используется dispatcher для решения mac vs sonnet fallback.
      operationId: miniappHeartbeatStatus
      responses:
        '200':
          description: Pool state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PoolInfo'

  /health:
    get:
      tags: [health]
      summary: Health check (только miniapp_api и микросервисы 8771-8782)
      description: |
        Стандартный health от ServiceBase. Каждый сервис может расширять
        результат через override healthcheck() (astro проверяет swisseph,
        digest проверяет наличие БД, и т.д.).

        **ВНИМАНИЕ:** этот endpoint существует на miniapp_api (8780) и на
        каждом из 10 микросервисов (8771-8782), но НЕ на jarvis-main (8765).
        У jarvis-main `GET /health` возвращает 404 — там нет ServiceBase.
        Для проверки живости jarvis-main используйте `GET /tasks/next`
        (он отдаёт 200 OK с `{}` при пустой очереди).
      operationId: healthCheck
      responses:
        '200':
          description: Сервис жив
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
        '404':
          description: |
            Endpoint не существует на jarvis-main (8765). Применимо только
            если servers[1] (jarvis-main) выбран как target.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ────────────────────────────────────────────────────────────────────────
  # jarvis-main (port 8765) — главный роутер
  # ────────────────────────────────────────────────────────────────────────
  /tasks/next:
    get:
      tags: [main]
      summary: Mac поллер забирает следующую задачу
      description: |
        Main jarvis (8765). Mac-пуллер периодически дёргает endpoint и забирает
        первую задачу со status='pending'. Задача переводится в 'processing'.
        Если очередь пуста — возвращает пустой объект `{}`.
      operationId: tasksNext
      responses:
        '200':
          description: Следующая задача или пусто
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/TaskNextItem'
                  - type: object
                    additionalProperties: false
                    description: Очередь пуста

  /callback:
    post:
      tags: [main]
      summary: Mac возвращает результат
      description: |
        Mac отдаёт готовый ответ обратно. UPDATE task_queue SET status='done',
        result=<text>. Если есть ожидающий asyncio.Event — он триггерится.
      operationId: tasksCallback
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackRequest'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkResponse'
        '500':
          description: Ошибка обработки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkResponse'

  /notify:
    post:
      tags: [main]
      summary: Mac шлёт уведомление Кириллу в Telegram
      description: |
        Bot отправляет text юзеру ALLOWED_UID. parse_mode=HTML.
      operationId: notify
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotifyRequest'
      responses:
        '200':
          description: Отправлено или ошибка отправки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkResponse'

  /tasks/heartbeat:
    post:
      tags: [main]
      summary: Mac heartbeat для конкретной задачи (jarvis-main)
      description: |
        Mac во время длинной обработки шлёт heartbeat для task_id. Если
        задача отменена новым сообщением — VPS вернёт `{ok:true, cancel:true}`.
        ВНИМАНИЕ: реальный path в jarvis.py — `/heartbeat`, но он отличается
        от miniapp_api `/heartbeat`. Здесь это эндпоинт jarvis-main (порт 8765).
      operationId: tasksHeartbeat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskHeartbeatRequest'
      responses:
        '200':
          description: ok+cancel-флаг
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskHeartbeatResponse'
        '500':
          description: Ошибка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /settings:
    get:
      tags: [main]
      summary: Все jarvis_settings для Mac-сессии
      operationId: getSettings
      responses:
        '200':
          description: Настройки
          content:
            application/json:
              schema:
                type: object
                properties:
                  settings:
                    type: object
                    additionalProperties: {type: string}

  /context:
    get:
      tags: [main]
      summary: Полный контекст jarvis_context для Mac
      operationId: getContext
      responses:
        '200':
          description: Контекст
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages:
                    type: array
                    items:
                      type: object
                      properties:
                        role: {type: string}
                        content: {type: string}
                  total_chars: {type: integer}
                  count: {type: integer}

  /context/save_summary:
    post:
      tags: [main]
      summary: Mac отправляет сжатый summary + ID к удалению
      operationId: saveContextSummary
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [summary]
              properties:
                summary: {type: string}
                delete_ids:
                  type: array
                  items: {type: integer}
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {type: string, enum: [ok]}
                  count_after: {type: integer}
        '400':
          description: пустой summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /registry:
    get:
      tags: [main]
      summary: Реестр всех Jarvis микросервисов
      description: Метаданные из service_registry.registry_snapshot()
      operationId: getRegistry
      responses:
        '200':
          description: Снапшот реестра
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  # ────────────────────────────────────────────────────────────────────────
  # Per-service /service endpoints (по портам через servers)
  # ────────────────────────────────────────────────────────────────────────
  /service/astro:
    post:
      tags: [astro]
      summary: Натальная карта или синастрия
      description: |
        Реальный путь — POST `/service` на http://127.0.0.1:8771.
        В этом OpenAPI выделено как `/service/astro` ради уникальности
        operationId; вызывайте с `servers[2]` (port 8771) и path `/service`.
      operationId: astroService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AstroRequest'
            examples:
              natal:
                summary: Натальная карта
                value:
                  task_id: "t-1"
                  payload:
                    type: "natal"
                    person1:
                      name: "Кирилл"
                      birth_date: "1992-01-08"
                      birth_time: "05:45"
                      birth_city: "Островец"
                      tz: "Europe/Minsk"
              synastry:
                summary: Синастрия
                value:
                  task_id: "t-2"
                  payload:
                    type: "synastry"
                    person1: {name: "A", birth_date: "1992-01-08", birth_time: "05:45", birth_city: "Минск"}
                    person2: {name: "B", birth_date: "1990-06-15", birth_time: "12:00", birth_city: "Тбилиси"}
      responses:
        '200':
          description: Результат расчёта
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AstroResponse'

  /service/digest:
    post:
      tags: [digest]
      summary: Сводка чатов за N часов
      operationId: digestService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DigestRequest'
      responses:
        '200':
          description: Дайджест
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DigestResponse'

  /service/deck:
    post:
      tags: [deck]
      summary: Discovery презентаций Section 228
      operationId: deckService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeckRequest'
      responses:
        '200':
          description: Skill recommendation + список деков
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeckResponse'

  /service/wiki:
    post:
      tags: [wiki]
      summary: Поиск по s228-wiki
      operationId: wikiService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WikiRequest'
      responses:
        '200':
          description: Top-K секций
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WikiResponse'

  /service/finance:
    post:
      tags: [finance]
      summary: P&L / DevModel CSV preview
      operationId: financeService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FinanceRequest'
      responses:
        '200':
          description: Превью CSV
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FinanceResponse'

  /service/city:
    post:
      tags: [city]
      summary: Поиск по гайдам городов
      operationId: cityService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CityRequest'
      responses:
        '200':
          description: Top-3 секций
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CityResponse'

  /service/weather:
    post:
      tags: [weather]
      summary: Погода через Open-Meteo
      description: |
        ВНИМАНИЕ: реальный payload weather-сервиса принимает поле `text` (произвольная
        фраза с городом и горизонтом) и/или `city` (явное имя). Поле `days` НЕ
        используется напрямую — горизонт определяется парсером "сейчас/завтра/
        на N дней/выходные" из `text`. Описание задачи "city, days?" — упрощение.
      operationId: weatherService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WeatherRequest'
      responses:
        '200':
          description: Текстовое описание погоды
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WeatherResponse'

  /service/reminder:
    post:
      tags: [reminder]
      summary: Управление напоминаниями
      description: |
        Полный action-list: create/list/dismiss/snooze (а не только add/list/remove
        как в исходной задаче). Если payload.action пустой и есть text — дефолт
        action=create.
      operationId: reminderService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReminderRequest'
      responses:
        '200':
          description: Результат операции
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReminderResponse'

  /service/dispatcher:
    post:
      tags: [dispatcher]
      summary: Оркестратор сложных задач
      description: |
        Если Mac online (heartbeat <30s) — делегирует через task_queue.
        Иначе декомпозирует через Sonnet, вызывает локальные сервисы, синтезирует.
      operationId: dispatcherService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DispatcherRequest'
      responses:
        '200':
          description: Финальный ответ
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DispatcherResponse'

  /service/decisions:
    post:
      tags: [decisions]
      summary: CRUD очередь решений Кирилла
      description: |
        Реальный путь — POST `/service` на http://127.0.0.1:8782.
        Жёсткая очередь архитектурных/продуктовых решений (НЕ task_queue).
        Action определяется `payload.action`: add | list | get | update | done | delete | stats.

        - **add** — создать решение. Required: `title`. Optional: `context`, `priority` (1=high, 2=normal, 3=low; default 2), `tags`, `status` (default open).
        - **list** — список решений. Optional: `status` (фильтр), `limit` (default 50). Сортировка: priority ASC, created_at DESC.
        - **get** — одно решение по `id`.
        - **update** — обновить поля (`status`, `title`, `context`, `priority`, `tags`, `blocker`). Required: `id`.
        - **done** — закрыть решение (status=done, decided_at=now). Required: `id`. Optional: `note` (добавляется в context).
        - **delete** — удалить. Required: `id`.
        - **stats** — счётчики по статусам (open/in_progress/done/blocked/cancelled).

        Допустимые статусы: open, in_progress, done, blocked, cancelled.
        priority_label: 1→high, 2→normal, 3→low.
      operationId: decisionsService
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DecisionsRequest'
            examples:
              add:
                summary: add — создать новое решение
                value:
                  task_id: "d-1"
                  payload:
                    action: "add"
                    title: "Перейти с Sonnet на Opus для dispatcher"
                    context: "Sonnet даёт галлюцинации в декомпозиции"
                    priority: 1
                    tags: "arch,jarvis"
              list:
                summary: list — список решений
                value:
                  task_id: "d-2"
                  payload:
                    action: "list"
                    status: "open"
                    limit: 50
              get:
                summary: get — одно решение
                value:
                  task_id: "d-3"
                  payload: {action: "get", id: 1}
              update:
                summary: update — изменить status/priority/blocker
                value:
                  task_id: "d-4"
                  payload:
                    action: "update"
                    id: 1
                    status: "in_progress"
                    priority: 2
              done:
                summary: done — закрыть решение
                value:
                  task_id: "d-5"
                  payload: {action: "done", id: 1, note: "Реализовано в commit abc123"}
              delete:
                summary: delete — удалить
                value:
                  task_id: "d-6"
                  payload: {action: "delete", id: 1}
              stats:
                summary: stats — счётчики
                value:
                  task_id: "d-7"
                  payload: {action: "stats"}
      responses:
        '200':
          description: Результат action
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DecisionsResponse'

components:
  parameters:
    TaskId:
      name: task_id
      in: query
      description: Идентификатор задачи (любая строка, ServiceBase возвращает её обратно в response)
      required: false
      schema: {type: string, default: "?"}

  schemas:
    # ── базовые ServiceBase схемы ──────────────────────────────────────────
    ServiceRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id:
          type: string
          description: Произвольный идентификатор. Возвращается в response.
        payload:
          type: object
          description: Зависит от сервиса (см. конкретные схемы Request)
        context:
          type: object
          description: Опциональный контекст (ключ-значение)

    ServiceResponse:
      type: object
      required: [task_id, status, duration_ms]
      properties:
        task_id: {type: string}
        status:
          type: string
          enum: [ok, error]
        result:
          type: object
          description: Результат handle() — структура зависит от сервиса
        error:
          type: string
          description: Присутствует только при status=error
        duration_ms: {type: integer}
        next_service:
          type: string
          description: Чейнинг (если handle вернул `_next_service`)

    ErrorResponse:
      type: object
      required: [task_id, status, error, duration_ms]
      properties:
        task_id: {type: string}
        status: {type: string, enum: [error]}
        error: {type: string}
        duration_ms: {type: integer}

    OkResponse:
      type: object
      properties:
        ok: {type: boolean}
        error: {type: string}

    HealthResponse:
      type: object
      required: [service, uptime_s, pid, port, ok]
      properties:
        service: {type: string, description: Имя сервиса (NAME)}
        uptime_s: {type: integer}
        pid: {type: integer}
        port: {type: integer}
        ok: {type: boolean}
      additionalProperties: true
      description: |
        Базовые поля гарантированы. Сервисы могут добавлять свои:
        - astro: {kerykeion: bool, swisseph: bool}
        - digest: {db: string, db_exists: bool}
        - deck:   {decks_dir, decks_exists, skills_dir, skills_exists, decks_count, skills_count}
        - wiki:   {wiki_root: string, md_files: int}
        - city:   {city_root: string, city_files: string[]}
        - dispatcher: {anthropic_key_present: bool, known_services: string[]}
        - miniapp_api: {services_known: int, mac_online: bool}

    # ── miniapp_api ────────────────────────────────────────────────────────
    MiniAppServiceRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          required: [action]
          properties:
            action:
              type: string
              enum:
                - status
                - tasks_active
                - history
                - services_log
                - agents_active
                - limits
            days:
              type: integer
              description: Для action=history (1..30)
            service:
              type: string
              description: Для action=services_log
              enum: [astro, digest, deck, wiki, finance, city, weather, reminder, dispatcher, decisions]
            lines:
              type: integer
              description: Для action=services_log (1..1000)
        context:
          type: object

    MiniAppServiceResponse:
      type: object
      required: [task_id, status, duration_ms]
      properties:
        task_id: {type: string}
        status: {type: string, enum: [ok, error]}
        duration_ms: {type: integer}
        result:
          oneOf:
            - $ref: '#/components/schemas/StatusResult'
            - $ref: '#/components/schemas/TasksActiveResult'
            - $ref: '#/components/schemas/HistoryResult'
            - $ref: '#/components/schemas/ServicesLogResult'
            - $ref: '#/components/schemas/AgentsActiveResult'
            - $ref: '#/components/schemas/LimitsResult'
            - type: object
              properties:
                error: {type: string}

    StatusResult:
      type: object
      required: [services, main, pool, counts]
      properties:
        services:
          type: array
          items: {$ref: '#/components/schemas/ServiceStatus'}
        main: {$ref: '#/components/schemas/MainInfo'}
        pool: {$ref: '#/components/schemas/PoolInfo'}
        counts: {$ref: '#/components/schemas/CountsInfo'}

    ServiceStatus:
      type: object
      required: [name, port, ok]
      description: |
        Статус одного из 10 микросервисов. Имя совпадает с NAME сервиса:
        astro (8771), digest (8772), deck (8773), wiki (8775), finance (8776),
        city (8777), weather (8778), reminder (8779), dispatcher (8781),
        decisions (8782).
      properties:
        name:
          type: string
          enum: [astro, digest, deck, wiki, finance, city, weather, reminder, dispatcher, decisions]
        port: {type: integer}
        ok: {type: boolean}
        uptime_s: {type: integer}
        latency_ms: {type: integer}
        error: {type: string}

    MainInfo:
      type: object
      properties:
        pid: {type: integer}
        uptime_s: {type: integer}
        version: {type: string}
        active: {type: string, description: systemd ActiveState}
        error: {type: string}

    PoolInfo:
      type: object
      required: [alive, busy, total, mac_online]
      properties:
        alive: {type: integer}
        busy: {type: integer}
        total: {type: integer}
        mac_online: {type: boolean}
        last_heartbeat_age_s:
          type: [integer, "null"]

    CountsInfo:
      type: object
      properties:
        messages: {type: integer}
        tasks_active: {type: integer}
        reminders: {type: integer}
        error: {type: string}

    TasksActiveResult:
      type: object
      properties:
        tasks:
          type: array
          items: {$ref: '#/components/schemas/TaskActive'}
        error: {type: string}

    TaskActive:
      type: object
      required: [id, status, prompt_preview, created_at]
      properties:
        id: {type: integer}
        status: {type: string, enum: [pending, processing]}
        prompt_preview: {type: string, maxLength: 200}
        created_at: {type: string}
        processing_for_sec:
          type: [integer, "null"]

    AgentsActiveResult:
      type: object
      required: [agents]
      properties:
        agents:
          type: array
          items: {$ref: '#/components/schemas/AgentInfo'}
        error: {type: string}

    AgentInfo:
      type: object
      required: [id, agent_name, status, subtasks]
      properties:
        id: {type: integer}
        task_type: {type: string}
        agent_name: {type: string}
        prompt: {type: string, maxLength: 200}
        status: {type: string, enum: [pending, processing, running]}
        current_step: {type: string}
        progress_pct: {type: integer, minimum: 0, maximum: 100}
        steps_total: {type: integer}
        steps_done: {type: integer}
        started_at: {type: [string, "null"]}
        finished_at: {type: [string, "null"]}
        created_at: {type: string}
        subtasks:
          type: array
          items: {$ref: '#/components/schemas/SubtaskInfo'}

    SubtaskInfo:
      type: object
      required: [id, status]
      properties:
        id: {type: integer}
        task_type: {type: string}
        agent_name: {type: string}
        service_name: {type: string}
        current_step: {type: string}
        status: {type: string, enum: [pending, running, processing, done, error]}
        progress_pct: {type: integer}
        started_at: {type: [string, "null"]}
        finished_at: {type: [string, "null"]}
        created_at: {type: string}

    LimitsResult:
      type: object
      required: [today, week, month, by_model]
      properties:
        today: {$ref: '#/components/schemas/PeriodStats'}
        week: {$ref: '#/components/schemas/PeriodStats'}
        month: {$ref: '#/components/schemas/PeriodStats'}
        by_model:
          type: array
          items: {$ref: '#/components/schemas/ModelStats'}
        error: {type: string}

    PeriodStats:
      type: object
      required: [input_tokens, output_tokens, cost_usd, requests]
      properties:
        input_tokens: {type: integer}
        output_tokens: {type: integer}
        cost_usd: {type: number, format: double}
        requests: {type: integer}

    ModelStats:
      allOf:
        - type: object
          properties:
            model: {type: string}
        - $ref: '#/components/schemas/PeriodStats'

    HistoryResult:
      type: object
      required: [messages]
      properties:
        messages:
          type: array
          items: {$ref: '#/components/schemas/BotMessage'}
        error: {type: string}

    BotMessage:
      type: object
      required: [id, direction, text, created_at]
      properties:
        id: {type: integer}
        direction: {type: string, enum: [in, out]}
        text: {type: string}
        created_at: {type: string}

    ServicesLogResult:
      type: object
      required: [log_lines]
      properties:
        log_lines:
          type: array
          items: {type: string}
        error: {type: string}

    MacHeartbeatRequest:
      type: object
      properties:
        alive: {type: integer}
        busy: {type: integer}
        total: {type: integer}

    MacHeartbeatResponse:
      type: object
      required: [ok, ts]
      properties:
        ok: {type: boolean}
        ts: {type: integer, description: epoch seconds}

    # ── jarvis-main ────────────────────────────────────────────────────────
    TaskNextItem:
      type: object
      required: [task_id, task_type]
      properties:
        task_id: {type: integer}
        task_type: {type: string, description: "query|digest|analyze|step|dispatcher"}
        prompt: {type: string}
        image_b64: {type: [string, "null"]}
        image_media_type: {type: [string, "null"]}

    CallbackRequest:
      type: object
      required: [task_id, result]
      properties:
        task_id:
          oneOf:
            - {type: integer}
            - {type: string}
        result: {type: string, description: Финальный текст ответа от Mac}

    NotifyRequest:
      type: object
      required: [text]
      properties:
        text: {type: string, description: HTML-текст для отправки в Telegram}

    TaskHeartbeatRequest:
      type: object
      required: [task_id]
      properties:
        task_id:
          oneOf:
            - {type: string}
            - {type: integer}

    TaskHeartbeatResponse:
      type: object
      required: [ok]
      properties:
        ok: {type: boolean}
        cancel:
          type: boolean
          description: true если задачу отменили новым сообщением
        error: {type: string}

    # ── astro ─────────────────────────────────────────────────────────────
    AstroRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          required: [type, person1]
          properties:
            type: {type: string, enum: [natal, synastry], default: natal}
            person1: {$ref: '#/components/schemas/AstroPerson'}
            person2: {$ref: '#/components/schemas/AstroPerson'}
        context: {type: object}

    AstroPerson:
      type: object
      required: [birth_date]
      properties:
        name: {type: string, default: Person}
        birth_date: {type: string, format: date, description: "YYYY-MM-DD"}
        birth_time: {type: string, default: "12:00", description: "HH:MM"}
        birth_city: {type: string, default: Greenwich}
        city: {type: string, description: алиас birth_city}
        tz: {type: string, description: IANA timezone (Europe/Minsk и т.д.)}
        lat: {type: number}
        lng: {type: number}

    AstroResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result:
              oneOf:
                - $ref: '#/components/schemas/AstroNatalResult'
                - $ref: '#/components/schemas/AstroSynastryResult'

    AstroSubject:
      type: object
      required: [person, birth, planets, houses, ascendant, midheaven]
      properties:
        person: {type: string}
        birth:
          type: object
          properties:
            year: {type: integer}
            month: {type: integer}
            day: {type: integer}
            hour: {type: integer}
            minute: {type: integer}
            city: {type: string}
            nation: {type: [string, "null"]}
            tz: {type: string}
            lat: {type: number}
            lng: {type: number}
        planets:
          type: array
          items: {$ref: '#/components/schemas/AstroPlanet'}
        houses:
          type: array
          items: {$ref: '#/components/schemas/AstroHouse'}
        ascendant:
          type: object
          properties:
            sign: {type: string}
            degree: {type: number}
        midheaven:
          type: object
          properties:
            sign: {type: string}
            degree: {type: number}

    AstroPointName:
      type: string
      description: |
        Имена планет и точек, которые могут встретиться в `planets[]` (точки —
        Asc/Desc/MC/IC/Chiron/Lunar Nodes), а также в участниках `aspects[]`
        (`p1`, `p2`). kerykeion использует именно эти латинские имена с
        подчёркиваниями (для составных).
      enum:
        # классические планеты
        - Sun
        - Moon
        - Mercury
        - Venus
        - Mars
        - Jupiter
        - Saturn
        - Uranus
        - Neptune
        - Pluto
        # лунные узлы (True Node)
        - True_North_Lunar_Node
        - True_South_Lunar_Node
        - Mean_North_Lunar_Node
        - Mean_South_Lunar_Node
        # астероид
        - Chiron
        # угловые точки (cusps)
        - Ascendant
        - Descendant
        - Medium_Coeli
        - Imum_Coeli
        # короткие синонимы (на случай если клиент шлёт MC/IC)
        - MC
        - IC

    AstroPlanet:
      type: object
      description: |
        Запись `planets[]` содержит как сами планеты, так и углы карты
        (Ascendant/Descendant/MC/IC) и доп. точки (Chiron, True Lunar Nodes).
        Поле `house` для угловых точек может отсутствовать или быть пустым.
      properties:
        name:
          allOf:
            - $ref: '#/components/schemas/AstroPointName'
          description: Имя планеты или точки (см. AstroPointName)
        sign: {type: string}
        degree: {type: number}
        abs_degree: {type: number}
        house: {type: [integer, string]}
        retrograde: {type: boolean}

    AstroHouse:
      type: object
      properties:
        num: {type: integer}
        sign: {type: string}
        cusp_degree: {type: number}
        abs_degree: {type: number}

    AstroAspect:
      type: object
      description: |
        Аспект между двумя планетами/точками. Участники (`p1`, `p2`) берутся
        из набора AstroPointName — включая угловые точки Asc/Desc/MC/IC и
        южный узел True_South_Lunar_Node, которые НЕ всегда фигурируют в
        `planets[]`, но участвуют в аспектике.
      properties:
        p1:
          allOf:
            - $ref: '#/components/schemas/AstroPointName'
        p2:
          allOf:
            - $ref: '#/components/schemas/AstroPointName'
        p1_owner: {type: [string, "null"]}
        p2_owner: {type: [string, "null"]}
        type: {type: string, description: "conjunction/opposition/square/trine/sextile/..."}
        orb: {type: number}
        aspect_degrees: {type: number}
        movement: {type: [string, "null"]}

    AstroNatalResult:
      allOf:
        - $ref: '#/components/schemas/AstroSubject'
        - type: object
          required: [type, aspects]
          properties:
            type: {type: string, enum: [natal]}
            aspects:
              type: array
              items: {$ref: '#/components/schemas/AstroAspect'}

    AstroSynastryResult:
      type: object
      required: [type, person1, person2, aspects]
      properties:
        type: {type: string, enum: [synastry]}
        person1: {$ref: '#/components/schemas/AstroSubject'}
        person2: {$ref: '#/components/schemas/AstroSubject'}
        aspects:
          type: array
          items: {$ref: '#/components/schemas/AstroAspect'}

    # ── digest ────────────────────────────────────────────────────────────
    DigestRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          properties:
            hours: {type: integer, default: 24, minimum: 1, maximum: 336}
            format: {type: string, enum: [short, full], default: full}
            significance_threshold: {type: integer, default: 5}
        context: {type: object}

    DigestResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result: {$ref: '#/components/schemas/DigestResult'}

    DigestResult:
      type: object
      required: [hours, format, threshold, chats, significant_chats, reminders_pending, digest_text, generated_at]
      properties:
        hours: {type: integer}
        format: {type: string, enum: [short, full]}
        threshold: {type: integer}
        vip_chats:
          type: array
          items: {type: string}
        chats:
          type: array
          items: {$ref: '#/components/schemas/DigestChat'}
        significant_chats:
          type: array
          items: {type: string}
        reminders_pending:
          type: array
          items: {$ref: '#/components/schemas/DigestReminder'}
        digest_text: {type: string}
        generated_at: {type: string, format: date-time}

    DigestChat:
      type: object
      properties:
        chat_name: {type: string}
        msg_count: {type: integer}
        incoming: {type: integer}
        last_msg: {type: string, maxLength: 200}
        last_date: {type: string}
        last_from_me: {type: boolean}
        needs_reply: {type: boolean}
        last_incoming_age_h: {type: number}
        score: {type: integer}
        score_reasons:
          type: array
          items: {type: string}

    DigestReminder:
      type: object
      properties:
        id: {type: integer}
        text: {type: string}
        due_at: {type: string}

    # ── deck ──────────────────────────────────────────────────────────────
    DeckRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          properties:
            query: {type: string}
            client: {type: string}
        context: {type: object}

    DeckResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result: {$ref: '#/components/schemas/DeckResult'}

    DeckResult:
      type: object
      required: [query, available_templates, existing_decks, next_action]
      properties:
        query: {type: string}
        client: {type: [string, "null"]}
        available_templates:
          type: array
          items: {type: string}
        skills_detail:
          type: array
          items:
            type: object
            properties:
              name: {type: string}
              path: {type: string}
              description: {type: string}
        existing_decks:
          type: array
          items: {type: string}
        recommended_skill: {type: [string, "null"]}
        skill_path: {type: string}
        skill_path_mac: {type: string}
        client_context: {type: string}
        client_hits:
          type: array
          items: {type: string}
        next_action: {type: string}

    # ── wiki ──────────────────────────────────────────────────────────────
    WikiRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          properties:
            query: {type: string}
            topics:
              type: array
              items: {type: string}
              description: |
                Список путей внутри wiki (с/без префикса s228-wiki/, с/без .md).
                Если задан — query опционален, отдаётся intro каждого файла.
        context: {type: object}

    WikiResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result: {$ref: '#/components/schemas/WikiResult'}

    WikiResult:
      type: object
      properties:
        matched_files:
          type: array
          items: {type: string}
        snippets:
          type: array
          items: {$ref: '#/components/schemas/WikiSnippet'}
        synthesis: {type: string}
        query: {type: string}
        scanned_files: {type: integer}
        error: {type: string}

    WikiSnippet:
      type: object
      properties:
        file: {type: string}
        section: {type: string}
        score: {type: integer}
        text: {type: string}

    # ── finance ───────────────────────────────────────────────────────────
    FinanceRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          properties:
            query: {type: string}
            metric:
              type: [string, "null"]
              enum: [utilization, pnl, revenue, expenses, null]
        context: {type: object}

    FinanceResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result: {$ref: '#/components/schemas/FinanceResult'}

    FinanceResult:
      type: object
      properties:
        metric: {type: string}
        query: {type: string}
        files:
          type: array
          items: {$ref: '#/components/schemas/FinanceFile'}
        context:
          type: string
          description: |
            OPTIONAL. Возвращается только когда найден контекст в P&L/DevModel.
            При отсутствии релевантного контекста поле в response отсутствует.
        _error: {type: string}

    FinanceFile:
      type: object
      properties:
        name: {type: string}
        rows_total: {type: integer}
        preview:
          type: array
          items:
            type: array
            items: {type: string}
        error: {type: string}

    # ── city ──────────────────────────────────────────────────────────────
    CityRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          properties:
            query: {type: string}
            city: {type: string, description: "алиас или slug (lisbon, rome)"}
        context: {type: object}

    CityResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result: {$ref: '#/components/schemas/CityResult'}

    CityResult:
      type: object
      properties:
        city: {type: string}
        matched_files:
          type: array
          items: {type: string}
        snippets:
          type: array
          items: {$ref: '#/components/schemas/CitySnippet'}
        summary: {type: string}
        query: {type: string}
        known_cities:
          type: array
          items: {type: string}
        error: {type: string}

    CitySnippet:
      type: object
      properties:
        file: {type: string}
        section: {type: string}
        score: {type: integer}
        text: {type: string}

    # ── weather ───────────────────────────────────────────────────────────
    WeatherRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          description: |
            Реально handle() требует text ИЛИ city. days в исходной задаче
            упомянут — фактически горизонт парсится из text-фразы
            ("сейчас", "завтра", "на 3 дня", "выходные", "через N дней").
          properties:
            text: {type: string, description: "произвольная фраза, напр. 'погода в Лиссабоне завтра'"}
            city: {type: string, description: "явное имя города (имеет приоритет)"}
        context: {type: object}

    WeatherResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result: {$ref: '#/components/schemas/WeatherResult'}

    WeatherResult:
      type: object
      required: [message, found]
      properties:
        message: {type: string, description: одно-строчный человекочитаемый ответ}
        found: {type: boolean}
        city: {type: string}
        country: {type: string}
        mode: {type: string, enum: [current, day, forecast]}
        days: {type: integer}
        cache_hit:
          type: boolean
          description: |
            OPTIONAL. Поле появляется выборочно — только когда ответ обслужен
            из кеша Open-Meteo. При cache miss поле отсутствует в response.

    # ── reminder ──────────────────────────────────────────────────────────
    ReminderRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          description: |
            action: create|list|dismiss|snooze.
            create: text (обязателен), due_at (ISO, опционально, парсится из text).
            list: status (default pending), limit (default 50).
            dismiss/snooze: reminder_id (обязателен), snooze_hours (для snooze, default 2).
          properties:
            action:
              type: string
              enum: [create, list, dismiss, snooze]
            text: {type: string}
            due_at: {type: string, format: date-time}
            reminder_id: {type: integer}
            snooze_hours: {type: number}
            status: {type: string}
            limit: {type: integer}
        context: {type: object}

    ReminderResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result:
              oneOf:
                - $ref: '#/components/schemas/ReminderCreateResult'
                - $ref: '#/components/schemas/ReminderListResult'
                - $ref: '#/components/schemas/ReminderUpdateResult'

    ReminderCreateResult:
      type: object
      properties:
        reminder_id: {type: integer}
        text: {type: string}
        due_at: {type: [string, "null"]}
        parsed_from_text: {type: boolean}
        message: {type: string}

    ReminderListResult:
      type: object
      properties:
        count: {type: integer}
        items:
          type: array
          items:
            type: object
            properties:
              id: {type: integer}
              text: {type: string}
              due_at: {type: [string, "null"]}
              status: {type: string}
              created_at: {type: string}
        message: {type: string}

    ReminderUpdateResult:
      type: object
      properties:
        updated: {type: integer}
        reminder_id: {type: integer}
        due_at: {type: string}
        message: {type: string}

    # ── dispatcher ────────────────────────────────────────────────────────
    DispatcherRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          required: [query]
          properties:
            query: {type: string}
            force_mode:
              type: string
              enum: [mac, sonnet]
              description: Для тестов — обойти автоопределение
        context: {type: object}

    DispatcherResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result: {$ref: '#/components/schemas/DispatcherResult'}

    DispatcherResult:
      type: object
      description: |
        Результат оркестратора. Поля `final_answer` и `answer` дублируют друг
        друга для совместимости (новые клиенты используют `answer`, старые —
        `final_answer`). При методе `api-fallback-direct` декомпозиция не
        выполняется и поле `decomposition` отсутствует.
      properties:
        final_answer:
          type: string
          description: Финальный текст ответа (legacy-имя, для совместимости)
        answer:
          type: string
          description: Финальный текст ответа (актуальное имя, дублирует final_answer)
        method_used:
          type: string
          enum: [mac, sonnet_fallback, api-fallback-direct]
          description: |
            - **mac** — задача делегирована на Mac через task_queue.
            - **sonnet_fallback** — Mac offline, выполнено через Sonnet с декомпозицией.
            - **api-fallback-direct** — Sonnet вызван напрямую без декомпозиции
              (упрощённый fallback когда декомпозиция нецелесообразна).
        decomposition:
          type: array
          description: |
            Шаги декомпозиции. Присутствует только для `method_used=sonnet_fallback`.
            Для `mac` и `api-fallback-direct` — отсутствует или пустой массив.
          items: {$ref: '#/components/schemas/DispatcherStep'}
        duration_ms: {type: integer}
        error: {type: string}

    DispatcherStep:
      type: object
      properties:
        step: {type: integer}
        service: {type: string}
        input: {type: object}
        depends_on:
          type: array
          items: {type: integer}
        result: {type: object}
        error: {type: string}

    # ── decisions ─────────────────────────────────────────────────────────
    DecisionsRequest:
      type: object
      required: [task_id, payload]
      properties:
        task_id: {type: string}
        payload:
          type: object
          required: [action]
          description: |
            Поле `action` обязательно. Остальные поля action-specific:
            - add: title (req), context, priority (1/2/3), tags, status
            - list: status, limit
            - get/update/done/delete: id (req)
            - update: status, title, context, priority, tags, blocker
            - done: id, note
            - stats: —
          properties:
            action:
              type: string
              enum: [add, list, get, update, done, delete, stats]
            id:
              type: integer
              description: Required для get/update/done/delete
            title:
              type: string
              description: Required для add
            context:
              type: string
            priority:
              type: integer
              enum: [1, 2, 3]
              description: 1=high, 2=normal (default), 3=low
            priority_label:
              type: string
              enum: [high, normal, low]
              description: Human-readable приоритет (read-only, выводится сервисом)
            tags:
              type: string
              description: CSV строка тегов (напр. "arch,jarvis,perf")
            status:
              type: string
              enum: [open, in_progress, done, blocked, cancelled]
            blocker:
              type: string
              nullable: true
              description: Описание блокера (для status=blocked). null — снять блок.
            limit:
              type: integer
              default: 50
            note:
              type: string
              description: Для action=done — заметка дописывается в context
        context: {type: object}

    DecisionsResponse:
      allOf:
        - $ref: '#/components/schemas/ServiceResponse'
        - type: object
          properties:
            result:
              oneOf:
                - $ref: '#/components/schemas/DecisionsAddResult'
                - $ref: '#/components/schemas/DecisionsListResult'
                - $ref: '#/components/schemas/DecisionsGetResult'
                - $ref: '#/components/schemas/DecisionsUpdateResult'
                - $ref: '#/components/schemas/DecisionsDeleteResult'
                - $ref: '#/components/schemas/DecisionsStatsResult'

    DecisionsItem:
      type: object
      required: [id, title, status, priority]
      properties:
        id: {type: integer}
        title: {type: string}
        context: {type: [string, "null"]}
        status:
          type: string
          enum: [open, in_progress, done, blocked, cancelled]
        priority:
          type: integer
          enum: [1, 2, 3]
        priority_label:
          type: string
          enum: [high, normal, low]
        tags: {type: [string, "null"]}
        created_at: {type: string}
        updated_at: {type: string}
        started_at: {type: [string, "null"]}
        decided_at: {type: [string, "null"]}
        blocker: {type: [string, "null"]}

    DecisionsAddPayload:
      type: object
      required: [action, title]
      properties:
        action: {type: string, enum: [add]}
        title: {type: string}
        context: {type: string}
        priority: {type: integer, enum: [1, 2, 3], default: 2}
        tags: {type: string}
        status:
          type: string
          enum: [open, in_progress, done, blocked, cancelled]
          default: open

    DecisionsAddResult:
      type: object
      required: [id, decision]
      properties:
        id: {type: integer}
        decision: {$ref: '#/components/schemas/DecisionsItem'}
        message: {type: string}

    DecisionsListResult:
      type: object
      required: [count, items]
      properties:
        count: {type: integer}
        items:
          type: array
          items: {$ref: '#/components/schemas/DecisionsItem'}
        message: {type: string}

    DecisionsGetResult:
      type: object
      required: [found]
      properties:
        found: {type: boolean}
        decision: {$ref: '#/components/schemas/DecisionsItem'}
        message: {type: string}

    DecisionsUpdateResult:
      type: object
      properties:
        updated: {type: integer}
        decision: {$ref: '#/components/schemas/DecisionsItem'}
        message: {type: string}

    DecisionsDeleteResult:
      type: object
      properties:
        deleted: {type: integer}
        id: {type: integer}
        message: {type: string}

    DecisionsStatsResult:
      type: object
      required: [open, in_progress, done, blocked, cancelled]
      properties:
        open: {type: integer}
        in_progress: {type: integer}
        done: {type: integer}
        blocked: {type: integer}
        cancelled: {type: integer}
        message: {type: string}
