openapi: 3.1.0
info:
  title: Astro API
  version: 0.1.0
  description: >
    REST API поверх ядра астрологических расчётов (движок Swiss Ephemeris,
    полная встроенная точность `.se1`). Геоцентрические эклиптические положения,
    куспиды домов, углы и аспекты. Все временные метки — UTC (RFC 3339).
    Идентичный расчёт также выполняется на стороне клиента как WebAssembly,
    поэтому веб-приложение и API никогда не расходятся. CORS открыт: разрешён
    любой Origin (GET/POST/OPTIONS), браузерные клиенты с других доменов могут
    обращаться к API напрямую. Справочник для чтения: docs/API.md (доступен по
    /docs).
  license:
    name: Proprietary
servers:
  - url: https://astro.yuretz.ru
    description: Production
  - url: http://127.0.0.1:8431
    description: Local

paths:
  /health:
    get:
      summary: Проверка работоспособности и информация о движке
      operationId: getHealth
      responses:
        "200":
          description: Сервис работает
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Health" }

  /v1/charts/natal:
    post:
      summary: Расчёт натальной карты (радикса)
      operationId: postNatalChart
      description: >
        Проекция ответа (необязательно). `profile` выбирает пресет, который
        одновременно пропускает вычисление ненужных аналитических блоков и
        сокращает JSON: `raw` (по умолчанию, всё, побайтово идентично
        непроецированному ответу), `natal_interp` (набор для интерпретации:
        убирает громоздкую поблочную астрономию и блоки только для отображения,
        округляет до 2 знаков), `horary` (только необходимое для хорара),
        `calendar` (только положения + дома). `precision` переопределяет
        десятичное округление. `fields=+a,-b` включает отдельные блоки поверх
        пресета (голое `fields=a,b` — это явный белый список). Параметры запроса
        имеют приоритет над теми же ключами в теле запроса. Неизвестный `profile`
        даёт 422.
      parameters:
        - { in: query, name: profile, required: false, schema: { type: string, enum: [raw, natal_interp, horary, calendar] } }
        - { in: query, name: precision, required: false, schema: { type: integer, minimum: 0, maximum: 8 } }
        - { in: query, name: fields, required: false, schema: { type: string }, description: "Список через запятую; `+block`/`-block` переключают относительно пресета, голые имена образуют белый список." }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NatalRequest" }
            example:
              datetime_utc: "1990-05-15T09:20:00Z"
              location: { latitude: 55.7558, longitude: 37.6178 }
              house_system: placidus
              zodiac: tropical
      responses:
        "200":
          description: Рассчитанная карта
          content:
            application/json:
              schema: { $ref: "#/components/schemas/NatalChart" }
        "422":
          $ref: "#/components/responses/DomainError"

  /v1/charts/natal.svg:
    get:
      summary: Натальное колесо как отдельное SVG-изображение
      operationId: getNatalWheelSvg
      description: >
        Натальная карта, отрисованная как SVG-колесо (та же геометрия, шрифты и
        глифы, что и в веб-клиенте), пригодная для прямого использования как
        `<img src>`. Неподвижные звёзды нанесены сразу за кольцом зодиака, каждая
        с нативной всплывающей подсказкой `<title>`; передайте `stars=false`,
        чтобы их скрыть. Цветовую схему задаёт `theme` (`light` по умолчанию или
        `dark`).
      parameters:
        - { in: query, name: datetime_utc, required: true, schema: { type: string, format: date-time } }
        - { in: query, name: lat, required: false, schema: { type: number } }
        - { in: query, name: lon, required: false, schema: { type: number } }
        - { in: query, name: house_system, required: false, schema: { type: string } }
        - { in: query, name: zodiac, required: false, schema: { type: string, enum: [tropical, sidereal] } }
        - { in: query, name: title, required: false, schema: { type: string } }
        - { in: query, name: stars, required: false, schema: { type: boolean, default: true } }
        - { in: query, name: theme, required: false, schema: { type: string, enum: [light, dark], default: light } }
      responses:
        "200":
          description: Изображение колеса
          content:
            image/svg+xml:
              schema: { type: string }
        "422":
          $ref: "#/components/responses/DomainError"

  /v1/charts/transits:
    post:
      summary: Натальная карта + положения на второй момент, с взаимными аспектами
      operationId: postTransitsChart
      description: >
        Параметры запроса `?profile=&fields=&precision=` (только query, не в
        теле) шейпят вложенные карты `natal`/`transit` так же, как на
        /v1/charts/natal — пресет пропускает вычисление и сериализацию ненужных
        блоков обеих карт.
      parameters:
        - { in: query, name: profile, required: false, schema: { type: string, enum: [raw, natal_interp, horary, calendar] } }
        - { in: query, name: precision, required: false, schema: { type: integer, minimum: 0, maximum: 8 } }
        - { in: query, name: fields, required: false, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TransitsRequest" }
            example:
              natal:
                datetime_utc: "1990-05-15T09:20:00Z"
                location: { latitude: 55.7558, longitude: 37.6178 }
              transit: { datetime_utc: "2026-07-02T10:00:00Z" }
      responses:
        "200":
          description: Рассчитанные транзиты
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransitsChart" }
        "422":
          $ref: "#/components/responses/DomainError"

  /v1/charts/synastry:
    post:
      summary: Две натальные карты с взаимными аспектами между ними
      operationId: postSynastryChart
      description: >
        Кроме взаимных аспектов тел, отдаёт кросс-контакты планета↔угол
        (`a_to_b_angles`/`b_to_a_angles`, все четыре угла второй карты) и
        наложения домов. Параметры запроса `?profile=&fields=&precision=`
        (только query) шейпят вложенные карты `a`/`b`.
      parameters:
        - { in: query, name: profile, required: false, schema: { type: string, enum: [raw, natal_interp, horary, calendar] } }
        - { in: query, name: precision, required: false, schema: { type: integer, minimum: 0, maximum: 8 } }
        - { in: query, name: fields, required: false, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SynastryRequest" }
      responses:
        "200":
          description: Рассчитанная синастрия
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SynastryChart" }
        "422":
          $ref: "#/components/responses/DomainError"

  /v1/charts/solar:
    post:
      summary: Натальная карта + соляр на год, с взаимными аспектами
      operationId: postSolarChart
      description: >
        Параметры запроса `?profile=&fields=&precision=` (только query) шейпят
        вложенные карты `natal`/`solar`.
      parameters:
        - { in: query, name: profile, required: false, schema: { type: string, enum: [raw, natal_interp, horary, calendar] } }
        - { in: query, name: precision, required: false, schema: { type: integer, minimum: 0, maximum: 8 } }
        - { in: query, name: fields, required: false, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SolarRequest" }
            example:
              natal:
                datetime_utc: "1990-05-15T09:20:00Z"
                location: { latitude: 55.7558, longitude: 37.6178 }
              year: 2026
      responses:
        "200":
          description: Рассчитанный соляр
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SolarChart" }
        "422":
          $ref: "#/components/responses/DomainError"

  /v1/charts/return:
    post:
      summary: Карта возвращения (лунар / Сатурн / узловая) для любого тела
      operationId: postReturnChart
      description: >
        Параметры запроса `?profile=&fields=&precision=` (только query) шейпят
        вложенные карты `natal`/`returned`.
      parameters:
        - { in: query, name: profile, required: false, schema: { type: string, enum: [raw, natal_interp, horary, calendar] } }
        - { in: query, name: precision, required: false, schema: { type: integer, minimum: 0, maximum: 8 } }
        - { in: query, name: fields, required: false, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReturnRequest" }
      responses:
        "200": { description: Карта возвращения, content: { application/json: { schema: { $ref: "#/components/schemas/ReturnChart" } } } }
        "422": { $ref: "#/components/responses/DomainError" }

  /v1/charts/progressed:
    post:
      summary: Вторичные прогрессии / символические дирекции
      operationId: postProgressedChart
      description: >
        Параметры запроса `?profile=&fields=&precision=` (только query) шейпят
        вложенные карты `natal`/`progressed`.
      parameters:
        - { in: query, name: profile, required: false, schema: { type: string, enum: [raw, natal_interp, horary, calendar] } }
        - { in: query, name: precision, required: false, schema: { type: integer, minimum: 0, maximum: 8 } }
        - { in: query, name: fields, required: false, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProgressedRequest" }
      responses:
        "200": { description: Прогрессивная карта, content: { application/json: { schema: { $ref: "#/components/schemas/ProgressedChart" } } } }
        "422": { $ref: "#/components/responses/DomainError" }

  /v1/charts/directions/primary:
    post:
      summary: "Первичные дирекции (Плацидус, полудуги, зодиакальные)"
      operationId: postPrimaryDirections
      description: >
        Первичные дирекции методом Плацидуса (semi-arc). Сигнификаторы Asc/MC/
        Солнце/Луна, промиссоры — десять планет и их птолемеевы аспекты. Дуга
        прямого восхождения переводится в годы ключом Naibod (по умолчанию) или
        Ptolemy. Считается тропически; требуется место рождения.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PrimaryDirectionsRequest" }
      responses:
        "200": { description: Список дирекций по возрасту, content: { application/json: { schema: { $ref: "#/components/schemas/PrimaryDirections" } } } }
        "422": { $ref: "#/components/responses/DomainError" }

  /v1/aspect-hits:
    post:
      summary: Точные даты транзитных аспектов на интервале + окна орба
      operationId: postAspectHits
      description: >
        Транзитное тело задаётся `transit_body` (одно) ИЛИ `transit_bodies`
        (массив — батч: натальная карта считается один раз, каждое событие в
        `hits`/`angle_hits` несёт своё `transit_body`; один батч-вызов заменяет
        N последовательных).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AspectHitsRequest" }
      responses:
        "200": { description: Список попаданий, content: { application/json: { schema: { $ref: "#/components/schemas/AspectHits" } } } }
        "422": { $ref: "#/components/responses/DomainError" }

  /v1/moon/transit:
    post:
      summary: Календарь транзитной Луны (окна Луны без курса + путь по натальным домам)
      operationId: postMoonTransit
      description: >
        Транзитная Луна на интервале (≤ 400 дней, ~13 месяцев — календарный год
        с запасом за один вызов): окна Луны без курса (Луна больше не образует
        аспектов до своей следующей ингрессии) и путь Луны через натальные дома. То же нахождение корней, что и в `moon_void`; для
        пути по домам нужна натальная карта с указанным местом.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MoonTransitRequest" }
      responses:
        "200": { description: Календарь Луны, content: { application/json: { schema: { $ref: "#/components/schemas/MoonTransit" } } } }
        "422": { $ref: "#/components/responses/DomainError" }

  /v1/extras:
    get:
      summary: Ближайшие затмения и данные неподвижной звезды Алголь на момент
      operationId: getExtras
      parameters:
        - { name: jd, in: query, required: true, schema: { type: number }, description: "Юлианский день (UT)" }
        - { name: zodiac, in: query, required: false, schema: { type: string, enum: [tropical, sidereal] } }
      responses:
        "200":
          description: Дополнительные данные
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ExtrasResult" }

  /v1/calendar/global:
    get:
      summary: Небо года как один кэшированный объект (глобальный астрокалендарь)
      operationId: getGlobalCalendar
      description: >
        «Фон года», независимый от какой-либо натальной карты: ингрессии по
        знакам, ретроградные станции и периоды, мунданные (транзит×транзит)
        аспекты, фазы Луны и затмения на интервале дат. Один пакетный проход по
        эфемеридам — вычисляется один раз и кэшируется для всех. Каждое `exact` —
        истинное пересечение методом нахождения корней (момент UTC), а не оценка;
        раскладка по часовым поясам — задача клиента. Луна исключена из ингрессий
        и пар аспектов, если явно не указана в `bodies` (её ~146 ингрессий/год —
        вопрос конкретного пользователя/скоринга, а не фон года).
      parameters:
        - { name: from, in: query, required: true, schema: { type: string, format: date-time }, description: "начало интервала (RFC 3339, UTC)" }
        - { name: to, in: query, required: true, schema: { type: string, format: date-time }, description: "конец интервала (UTC); ≤ ~2 лет после `from`" }
        - { name: bodies, in: query, required: false, schema: { type: string }, description: "список через запятую; по умолчанию = sun..pluto (без Луны). Управляет ингрессиями + станциями; если задан явно, мунданные аспекты перебирают все пары среди этих тел." }
        - { name: aspects, in: query, required: false, schema: { type: string }, description: "список видов аспектов через запятую; по умолчанию = пять мажорных (conjunction,sextile,square,trine,opposition)." }
        - { name: zodiac, in: query, required: false, schema: { type: string, enum: [tropical, sidereal] } }
      responses:
        "200":
          description: Глобальный календарь
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GlobalCalendar" }
        "422":
          $ref: "#/components/responses/DomainError"

  /v1/geo/utc:
    post:
      summary: Местное гражданское время в часовом поясе IANA → UTC (исторический переход на летнее время)
      operationId: postGeoUtc
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UtcRequest" }
            example: { datetime_local: "1990-05-15T12:00", tz: "Europe/Moscow" }
      responses:
        "200":
          description: Преобразованный момент
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UtcResult" }
        "422":
          $ref: "#/components/responses/DomainError"

  /v1/geo/search:
    get:
      summary: Поиск города для выбора места рождения (42 языка)
      operationId: getGeoSearch
      parameters:
        - { name: q, in: query, required: true, schema: { type: string } }
        - { name: limit, in: query, required: false, schema: { type: integer, default: 8, maximum: 20 } }
        - { name: lang, in: query, required: false, schema: { type: string, default: ru } }
      responses:
        "200":
          description: Подходящие города, сначала совпадения по префиксу, затем по населению
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/GeoCity" }

  /v1/geo/near:
    get:
      summary: Ближайший город к координате (обратное геокодирование)
      operationId: getGeoNear
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number } }
        - { name: lon, in: query, required: true, schema: { type: number } }
        - { name: lang, in: query, required: false, schema: { type: string, default: ru } }
      responses:
        "200":
          description: Ближайший город (или null)
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/GeoCity" }
                  - { type: "null" }

  /v1/geo/tzcity:
    get:
      summary: Самый населённый город в часовом поясе IANA
      operationId: getGeoTzCity
      parameters:
        - { name: tz, in: query, required: true, schema: { type: string } }
        - { name: lang, in: query, required: false, schema: { type: string, default: ru } }
      responses:
        "200":
          description: Город (или null)
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/GeoCity" }
                  - { type: "null" }

  /v1/geo/pack:
    get:
      summary: "Офлайн-пакет городов для каждого языка (бинарный, gzip) — внутренний"
      operationId: getGeoPack
      parameters:
        - { name: lang, in: query, required: true, schema: { type: string } }
        - { name: v, in: query, required: false, schema: { type: string }, description: "сброс кэша (игнорируется на сервере)" }
      responses:
        "200":
          description: "Колоночный пакет V3, всегда Content-Encoding: gzip"
          content:
            text/tab-separated-values: {}
        "304":
          description: Не изменено (ETag)

components:
  responses:
    DomainError:
      description: Неверный ввод, или дома не определены для данной широты
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Health:
      type: object
      properties:
        status: { type: string, example: ok }
        service: { type: string, example: astro }
        engine: { type: string }
        precision: { type: string }
    Error:
      type: object
      properties:
        error: { type: string }
      required: [error]
    GeoLocation:
      type: object
      properties:
        latitude: { type: number, description: "градусы, положительные к северу" }
        longitude: { type: number, description: "градусы, положительные к востоку" }
        altitude: { type: number, default: 0, description: метры }
      required: [latitude, longitude]
    HouseSystem:
      type: string
      enum: [koch, placidus, whole_sign, equal, regiomontanus, campanus]
      default: koch
    Zodiac:
      type: string
      enum: [tropical, sidereal]
      default: tropical
    Body:
      type: string
      enum:
        [sun, moon, mercury, venus, mars, jupiter, saturn, uranus, neptune, pluto,
         mean_node, true_node, mean_lilith, true_lilith, chiron, south_node, white_moon, selena, proserpina]
    Sign:
      type: string
      enum:
        [aries, taurus, gemini, cancer, leo, virgo, libra, scorpio, sagittarius, capricorn, aquarius, pisces]
    DayNightRulers:
      type: object
      properties:
        day: { $ref: "#/components/schemas/Body" }
        night: { $ref: "#/components/schemas/Body" }
      required: [day, night]
    DomicileRulersFull:
      type: object
      description: "полная таблица: два массива по 12 тел, индекс Овен..Рыбы"
      properties:
        day: { type: array, minItems: 12, maxItems: 12, items: { $ref: "#/components/schemas/Body" } }
        night: { type: array, minItems: 12, maxItems: 12, items: { $ref: "#/components/schemas/Body" } }
      required: [day, night]
    DomicileRulersSparse:
      type: object
      description: "разреженная таблица: ключи — имена знаков (aries..pisces), значения — {day,night}; неуказанные знаки берут современного одиночного управителя"
      additionalProperties: { $ref: "#/components/schemas/DayNightRulers" }
    Dignity:
      type: string
      enum: [domicile, exaltation, detriment, fall, peregrine]
    ObjectPosition:
      type: object
      properties:
        body: { $ref: "#/components/schemas/Body" }
        longitude: { type: number, description: "эклиптическая долгота [0,360)" }
        speed_longitude: { type: number, description: "град/день; отрицательно = ретроградный" }
        sign: { $ref: "#/components/schemas/Sign" }
        degrees_in_sign: { type: number }
        retrograde: { type: boolean }
        dignity: { $ref: "#/components/schemas/Dignity", description: "западная классическая; только десять планет; отсутствует для математических точек и Хирона" }
        dignity_school:
          type: object
          description: "достоинство школы Ананды (отдельная система); присутствует для десяти планет, Хирона, Прозерпины и узлов"
          properties:
            category: { type: string, enum: [domicile_major, domicile_minor, exaltation, kinship, neutral, enmity, fall, detriment] }
            score: { type: integer, minimum: 0, maximum: 6, description: "обитель 6 · экзальтация 5 · родство 4 · нейтрально 3 · вражда 2 · падение 1 · изгнание 0" }
        term_ruler: { $ref: "#/components/schemas/Body", description: "позиционный; присутствует для всех объектов" }
        decan_ruler: { $ref: "#/components/schemas/Body", description: "позиционный; присутствует для всех объектов" }
        speed_class: { type: string, enum: [fast, slow], description: "только физические тела; отсутствует для математических точек" }
        stationary: { type: boolean, description: "только физические тела; отсутствует для математических точек" }
        house: { type: integer, minimum: 1, maximum: 12, description: "присутствует, когда в карте есть дома" }
        dispositor: { $ref: "#/components/schemas/Body", description: "основной управитель знака объекта по схеме запроса" }
        solar_status: { type: string, enum: [cazimi, combust, under_beams, free], description: "только десять планет; отсутствует для математических точек и Хирона" }
        declination: { type: number, description: "экваториальное склонение, градусы" }
        unaspected: { type: boolean, description: "не образует птолемеевского аспекта с другой из десяти планет (квинконс исключён); присутствует для десяти планет" }
    Houses:
      type: object
      properties:
        system: { $ref: "#/components/schemas/HouseSystem" }
        cusps: { type: array, items: { type: number }, minItems: 12, maxItems: 12 }
        ascendant: { type: number }
        midheaven: { type: number }
        descendant: { type: number, description: "Десцендент = asc + 180°" }
        ic: { type: number, description: "Imum Coeli = mc + 180°" }
        vertex: { oneOf: [{ type: number }, { type: "null" }] }
        intercepted_signs: { type: array, items: { $ref: "#/components/schemas/Sign" } }
        house_rulers:
          type: array
          items:
            type: object
            properties:
              house: { type: integer, minimum: 1, maximum: 12 }
              ruler: { type: array, items: { $ref: "#/components/schemas/Body" }, description: "управитель(и) знака на куспиде; 1–2 (двойные обители modern по секте или оси spbaa)" }
              co_rulers: { type: array, items: { $ref: "#/components/schemas/Body" } }
              almuten: { $ref: "#/components/schemas/Body" }
    AspectHit:
      type: object
      properties:
        a: { $ref: "#/components/schemas/Body" }
        b: { $ref: "#/components/schemas/Body" }
        kind: { type: string, enum: [conjunction, sextile, square, trine, opposition, quincunx] }
        angle: { type: number, description: "0/60/90/120/150/180" }
        orb: { type: number, description: "отклонение от точного, градусы" }
        applying: { type: boolean, description: "true = сходящийся (орб сокращается), false = расходящийся" }
    NatalRequest:
      type: object
      properties:
        datetime_utc: { type: string, format: date-time }
        location:
          oneOf: [{ $ref: "#/components/schemas/GeoLocation" }, { type: "null" }]
        house_system: { $ref: "#/components/schemas/HouseSystem" }
        zodiac: { $ref: "#/components/schemas/Zodiac" }
        objects:
          type: array
          items: { $ref: "#/components/schemas/Body" }
        rulership_scheme:
          type: string
          enum: [traditional, modern, spbaa]
          default: modern
          description: "схема для sign_rulers, house_rulers и диспозитора каждого объекта. modern: три знака с секта-зависимыми двойными обителями (Скорпион день Плутон/ночь Марс, Козерог день Сатурн/ночь Уран, Рыбы день Нептун/ночь Юпитер), активный по секте первым; Водолей — фиксированная пара [uranus, saturn] (Уран всегда старший, Сатурн всегда со-управитель, секта порядок НЕ меняет). Двойные обители не влияют на поле dignity объекта (оно классическое одиночное)"
        domicile_rulers:
          description: "своя день/ночь-таблица обителей; переопределяет rulership_scheme для dignity/dispositor/sign_rulers/house_rulers/receptions/альмутенов (оба владыки знака = обитель, активный по секте). Формат: полный (day[12]/night[12]) или разреженный (sign->{day,night})"
          oneOf:
            - $ref: "#/components/schemas/DomicileRulersFull"
            - $ref: "#/components/schemas/DomicileRulersSparse"
        wide_aspects:
          type: boolean
          default: false
          description: "также выдавать wide_aspects/wide_angle_aspects при орбах по соглашению (мажорные 6°, светила 8°, квинконс 3°) с зеркалами Dsc/IC"
        orbs:
          type: object
          additionalProperties: { type: number }
          description: "допуски орба для каждого тела (классический метод полусумм орбов — moiety) для пяти птолемеевых аспектов; квинконс сохраняет фиксированный орб 3° (не отбрасывается). Присутствует (даже {}) → аспекты используют их; частичная карта, неуказанные тела сохраняют классические значения по умолчанию (Солнце 15°, Луна 12°, …). См. /docs/orbs"
        minor_aspects:
          type: boolean
          default: false
          description: "также выдавать minor_aspects — гармонические минорные (semisextile, semisquare, quintile, семейства septile/novile, …) при узких орбах"
        asc_longitude:
          type: number
          description: "кармическая карта: строить дома от ЭТОЙ долготы Асцендента вместо горизонта (например, ASC = Восходящий узел, метод Шестопалова). Система домов сохраняется — для широты места решается соответствующий ARMC, куспиды Коха/Плацидуса остаются осмысленными. Требует location; только тропический зодиак"
        profile:
          type: string
          enum: [raw, natal_interp, horary, calendar]
          description: "пресет проекции ответа (см. описание эндпоинта); параметр запроса `?profile=` имеет приоритет над этим"
        precision:
          type: integer
          minimum: 0
          maximum: 8
          description: "десятичное округление для числовых полей; переопределяет значение пресета по умолчанию"
        fields:
          type: string
          description: "`+block`/`-block` переключают поверх пресета, либо голый белый список `a,b`"
      required: [datetime_utc]
    NatalChart:
      type: object
      properties:
        datetime_utc: { type: string, format: date-time }
        julian_day_ut: { type: number }
        zodiac: { $ref: "#/components/schemas/Zodiac" }
        objects: { type: array, items: { $ref: "#/components/schemas/ObjectPosition" } }
        aspects: { type: array, items: { $ref: "#/components/schemas/AspectHit" } }
        sign_rulers:
          type: array
          minItems: 12
          maxItems: 12
          description: "управитель(и) каждого знака 0..11 по схеме запроса; каждый элемент — массив из 1–2 тел (два на двойных обителях modern по секте и осях spbaa; активный первым)"
          items: { type: array, items: { $ref: "#/components/schemas/Body" } }
        sect: { type: string, enum: [diurnal, nocturnal] }
        almuten: { $ref: "#/components/schemas/Almuten" }
        final_dispositor: { $ref: "#/components/schemas/Body", description: "финальный диспозитор эффективной схемы, либо null" }
        moon_phase: { type: string, enum: [waxing, waning] }
        moon_void:
          type: object
          properties:
            void: { type: boolean }
            start: { type: string, description: "начало холостого окна (последний точный аспект Луны в знаке, либо её вход в знак, если аспектов не было); только при void=true" }
            until: { type: string }
            next_sign_at: { type: string }
            via_combusta: { type: boolean, description: "Луна в Via Combusta (15° Весов – 15° Скорпиона)" }
            spica_exception: { type: boolean, description: "Луна в соединении со Спикой (±1°) — средневековая защита Луны без курса/поражённой Луны" }
        moon_sequence: { $ref: "#/components/schemas/MoonSequence" }
        planetary_hour: { $ref: "#/components/schemas/PlanetaryHour" }
        lunar_day: { $ref: "#/components/schemas/LunarDay" }
        declination_aspects:
          type: array
          items: { $ref: "#/components/schemas/DeclAspect" }
        angle_aspects:
          type: array
          description: "аспекты от объектов ко всем четырём углам карты (Asc/MC/Dsc/IC); Dsc/IC считаются прямо, каждый хит держит орб по умолчанию своего вида аспекта; присутствует при наличии домов"
          items: { $ref: "#/components/schemas/AngleAspect" }
        orbs:
          type: object
          additionalProperties: { type: number }
          description: "фактически применённые эффективные допуски орба для каждого тела (присутствует только в режиме полусумм орбов — moiety)"
        minor_aspects:
          type: array
          description: "гармонические минорные аспекты (по запросу minor_aspects)"
          items: { $ref: "#/components/schemas/MinorAspectHit" }
        wide_aspects:
          type: array
          description: "внутрикарточные аспекты при орбах по соглашению (по запросу wide_aspects); derived=в расширенной полосе"
          items: { $ref: "#/components/schemas/WideAspect" }
        wide_angle_aspects:
          type: array
          description: "аспекты при орбах по соглашению ко всем четырём углам, включая зеркала Dsc/IC (по запросу wide_aspects)"
          items: { $ref: "#/components/schemas/WideAngleAspect" }
        configurations:
          type: array
          description: "конфигурации аспектов (большой трин/крест, тау-квадрат, йод, стеллиум, …)"
          items: { $ref: "#/components/schemas/Configuration" }
        chart_shape: { $ref: "#/components/schemas/ChartShape" }
        star_conjunctions:
          type: array
          description: "соединения неподвижных звёзд с планетами и углами карты"
          items: { $ref: "#/components/schemas/StarConjunction" }
        star_positions:
          type: array
          description: "полный каталог неподвижных звёзд с долготами, для нанесения звёзд вокруг колеса (в стиле ZET)"
          items: { $ref: "#/components/schemas/StarPosition" }
        receptions: { $ref: "#/components/schemas/Receptions" }
        disposition: { $ref: "#/components/schemas/Disposition" }
        houses: { $ref: "#/components/schemas/Houses" }
    ChartShape:
      type: object
      description: "планетарный узор Марка Эдмунда Джонса — общая форма карты"
      properties:
        pattern:
          type: string
          enum: [splash, bundle, bowl, bucket, locomotive, seesaw, splay]
        occupied_arc: { type: number, description: "дуга, охватываемая планетами (360 − наибольший разрыв), градусы" }
        largest_gap: { type: number, description: "наибольший пустой разрыв между соседними планетами, градусы" }
        leading: { $ref: "#/components/schemas/Body", description: "планета после наибольшего разрыва (чаша/локомотив/ведро)" }
        handle: { type: array, items: { $ref: "#/components/schemas/Body" }, description: "планета(ы) ручки ведра" }
    MoonSequence:
      type: object
      description: "последовательность аспектов Луны в пределах её текущего знака (хорар)"
      properties:
        last:
          type: object
          description: "последний точный аспект до момента карты (в пределах текущего знака), либо null"
          properties:
            body: { $ref: "#/components/schemas/Body" }
            kind: { type: string, enum: [conjunction, sextile, square, trine, opposition] }
            exact: { type: string, format: date-time }
        upcoming:
          type: array
          description: "будущие точные аспекты до ингрессии в знак, по возрастанию; пусто тогда и только тогда, когда moon_void.void"
          items:
            type: object
            properties:
              body: { $ref: "#/components/schemas/Body" }
              kind: { type: string, enum: [conjunction, sextile, square, trine, opposition] }
              exact: { type: string, format: date-time }
        ingress_at: { type: string, format: date-time, description: "= moon_void.next_sign_at" }
    PlanetaryHour:
      type: object
      description: "планетный час + управители дня/часа (классические неравные часы от восхода)"
      properties:
        day_ruler: { $ref: "#/components/schemas/Body" }
        hour_ruler: { $ref: "#/components/schemas/Body" }
        hour_index: { type: integer, minimum: 1, maximum: 24, description: "1-12 день, 13-24 ночь" }
        is_day: { type: boolean }
        sunrise: { type: string, format: date-time }
        sunset: { type: string, format: date-time }
        next_sunrise: { type: string, format: date-time }
    LunarDay:
      type: object
      description: "лунный день 1–30 ПО ВОСХОДУ ЛУНЫ в месте карты (школа Вронского/Величко): день 1 = новолуние → первый восход Луны, каждый следующий день начинается с восхода Луны, последний день цикла (29/30) кончается следующим новолунием. Локация-зависим; отсутствует на полярных широтах без восхода Луны. Блок fields: lunar_day (в профиле horary выключен)"
      properties:
        number: { type: integer, minimum: 1, maximum: 30 }
        day_start: { type: string, format: date-time, description: "начало текущего лунного дня (новолуние для дня 1, иначе восход Луны)" }
        day_end: { type: string, format: date-time, description: "конец (следующий восход Луны, либо новолуние для последнего дня цикла)" }
        new_moon: { type: string, format: date-time, description: "новолуние, открывшее текущий цикл" }
        moonset: { type: string, format: date-time, description: "ближайший заход Луны после момента карты (справочно)" }
    StarConjunction:
      type: object
      description: "неподвижная звезда в соединении с планетой или углом карты (по эклиптической долготе)"
      properties:
        body: { $ref: "#/components/schemas/Body", description: "планета в соединении со звездой (взаимоисключающе с angle)" }
        angle: { type: string, enum: [ascendant, midheaven, descendant, imum_coeli] }
        star: { type: string, example: "Regulus" }
        star_longitude: { type: number, description: "тропическая долгота, прецессированная на дату карты" }
        orb: { type: number, description: "отклонение от точного, градусы" }
        magnitude: { type: number, description: "видимая звёздная величина (меньше = ярче)" }
    StarPosition:
      type: object
      description: "эклиптическая долгота неподвижной звезды на дату карты (для нанесения вокруг колеса)"
      properties:
        star: { type: string, example: "Aldebaran" }
        longitude: { type: number, description: "тропическая долгота, прецессированная на дату карты" }
        magnitude: { type: number, description: "видимая звёздная величина (меньше = ярче)" }
        anchor: { type: boolean, description: "постоянный якорь колеса (Королевская звезда / Алголь / Спика); рендереры наносят звезду при anchor ИЛИ соединении" }
    Almuten:
      type: object
      description: "альмутен карты (Ибн Эзра) с воспроизводимой разбивкой баллов"
      properties:
        winner: { $ref: "#/components/schemas/Body" }
        method: { type: string, example: "ibn_ezra" }
        points_used: { type: array, items: { type: string }, description: "оценённые хилегические точки" }
        scores:
          type: array
          items:
            type: object
            properties:
              body: { $ref: "#/components/schemas/Body" }
              score: { type: integer }
        tie: { type: boolean }
    AngleAspect:
      type: object
      properties:
        body: { $ref: "#/components/schemas/Body" }
        angle: { type: string, enum: [ascendant, midheaven, descendant, imum_coeli] }
        kind: { type: string, enum: [conjunction, sextile, square, trine, opposition, quincunx] }
        exact_angle: { type: number, description: "0/60/90/120/150/180" }
        orb: { type: number, description: "отклонение от точного, градусы" }
    MinorAspectHit:
      type: object
      description: "гармонический минорный аспект между двумя телами"
      properties:
        a: { $ref: "#/components/schemas/Body" }
        b: { $ref: "#/components/schemas/Body" }
        kind: { type: string, enum: [semisextile, decile, novile, semisquare, septile, quintile, binovile, biseptile, tridecile, sesquiquadrate, biquintile, triseptile, quadranovile] }
        angle: { type: number }
        orb: { type: number }
        applying: { type: boolean }
    WideAspect:
      type: object
      description: "внутрикарточный аспект при орбах по соглашению"
      properties:
        a: { $ref: "#/components/schemas/Body" }
        b: { $ref: "#/components/schemas/Body" }
        kind: { type: string, enum: [conjunction, sextile, square, trine, opposition, quincunx] }
        angle: { type: number }
        orb: { type: number }
        derived: { type: boolean, description: "только в расширенной полосе (за пределами орба по умолчанию); отсутствует, когда false" }
    WideAngleAspect:
      type: object
      description: "аспект при орбе по соглашению к углу карты (все четыре)"
      properties:
        body: { $ref: "#/components/schemas/Body" }
        angle: { type: string, enum: [ascendant, midheaven, descendant, imum_coeli] }
        kind: { type: string, enum: [conjunction, sextile, square, trine, opposition, quincunx] }
        exact_angle: { type: number }
        orb: { type: number }
        derived: { type: boolean, description: "за пределами орба по умолчанию, либо зеркало Dsc/IC; отсутствует, когда false" }
    Configuration:
      type: object
      properties:
        type:
          type: string
          enum: [grandCross, trapezoid, cart, sail, grandTrine, stellium, yod, tSquare, bisextile]
        bodies: { type: array, items: { $ref: "#/components/schemas/Body" } }
        apex: { $ref: "#/components/schemas/Body", description: "фокальное тело тау-квадрата/йода/бисекстиля" }
        axis:
          type: array
          description: "два противоположных полюса тау-квадрата (каждый — список тел)"
          items: { type: array, items: { $ref: "#/components/schemas/Body" } }
        edges:
          type: array
          items:
            type: object
            properties:
              a: { $ref: "#/components/schemas/Body" }
              b: { $ref: "#/components/schemas/Body" }
        scope: { type: string, enum: [sign, house], description: "область стеллиума" }
        sign: { $ref: "#/components/schemas/Sign" }
        house: { type: integer, minimum: 1, maximum: 12 }
        attendants:
          type: array
          description: "точки (узлы/Лилит/Хирон/жребии) в знаке/доме стеллиума — не учитываются в кворуме"
          items: { $ref: "#/components/schemas/Body" }
    Reception:
      type: object
      properties:
        a: { $ref: "#/components/schemas/Body" }
        b: { $ref: "#/components/schemas/Body" }
        scheme: { type: string, enum: [traditional, modern], description: "только рецепции по управлению" }
        aspect:
          type: object
          description: "аспект между парой, если есть"
          properties:
            kind: { type: string, enum: [conjunction, sextile, square, trine, opposition, quincunx] }
            orb: { type: number }
    Receptions:
      type: object
      description: "взаимные рецепции среди десяти планет"
      properties:
        by_rulership:
          type: array
          description: "каждая пара помечена схемой (traditional/modern/effective), при которой она действует"
          items: { $ref: "#/components/schemas/Reception" }
        by_exaltation: { type: array, items: { $ref: "#/components/schemas/Reception" } }
        by_face: { type: array, items: { $ref: "#/components/schemas/Reception" } }
    DispositionNode:
      type: object
      properties:
        body: { $ref: "#/components/schemas/Body" }
        disposes_to: { oneOf: [{ $ref: "#/components/schemas/Body" }, { type: "null" }], description: "диспонирующая планета; null, когда у знака нет управителя в схеме (граф экзальтаций)" }
    ChainSink:
      type: object
      description: "куда стекает цепочка диспозиторов"
      properties:
        type: { type: string, enum: [terminal, loop] }
        id:
          description: "тело (терминал) или список тел (петля)"
          oneOf: [{ $ref: "#/components/schemas/Body" }, { type: array, items: { $ref: "#/components/schemas/Body" } }]
    Chain:
      type: object
      properties:
        body: { $ref: "#/components/schemas/Body" }
        path: { type: array, items: { $ref: "#/components/schemas/Body" }, description: "планета → каждый диспозитор → узел-сток" }
        sink: { $ref: "#/components/schemas/ChainSink" }
    DispositionScheme:
      type: object
      description: "функциональный граф: у каждой планеты один диспозитор, каждая компонента содержит ровно один цикл"
      properties:
        nodes: { type: array, items: { $ref: "#/components/schemas/DispositionNode" } }
        terminals: { type: array, items: { $ref: "#/components/schemas/Body" }, description: "самодиспонирующие планеты (циклы длины 1)" }
        loops:
          type: array
          description: "циклы длины ≥ 2 (длина 2 = взаимная рецепция)"
          items: { type: array, items: { $ref: "#/components/schemas/Body" } }
        chains: { type: array, items: { $ref: "#/components/schemas/Chain" } }
        final_dispositor: { oneOf: [{ $ref: "#/components/schemas/Body" }, { type: "null" }], description: "существует тогда и только тогда, когда ровно один терминал и нет петель" }
    Disposition:
      type: object
      description: "дерево диспозиции: схема modern (эффективная, с учётом domicile_rulers) + справочная spbaa, плюс граф дебилитации by_detriment"
      properties:
        schemes:
          type: object
          properties:
            modern: { $ref: "#/components/schemas/DispositionScheme" }
            spbaa: { $ref: "#/components/schemas/SpbaaScheme" }
        by_detriment: { $ref: "#/components/schemas/DispositionScheme" }
    SpbaaScheme:
      type: object
      description: "диспозиция СПбАА — граф с несколькими родителями (у планеты может быть два диспозитора). Экспериментально; петли/цепочки отложены."
      properties:
        nodes:
          type: array
          items:
            type: object
            properties:
              body: { $ref: "#/components/schemas/Body" }
              disposes_to: { type: array, items: { $ref: "#/components/schemas/Body" }, description: "1–2 диспозитора" }
        terminals: { type: array, items: { $ref: "#/components/schemas/Body" }, description: "планеты в знаке, которым сами управляют" }
        experimental: { type: boolean }
    Positions:
      type: object
      properties:
        datetime_utc: { type: string, format: date-time }
        julian_day_ut: { type: number }
        objects: { type: array, items: { $ref: "#/components/schemas/ObjectPosition" } }
    MomentRequest:
      type: object
      properties:
        datetime_utc: { type: string, format: date-time }
        location:
          oneOf: [{ $ref: "#/components/schemas/GeoLocation" }, { type: "null" }]
      required: [datetime_utc]
    TransitsRequest:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalRequest" }
        transit: { $ref: "#/components/schemas/MomentRequest" }
        orb: { type: number, description: "единый максимальный орб для всех аспектов" }
      required: [natal, transit]
    TransitsChart:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalChart" }
        transit: { $ref: "#/components/schemas/Positions" }
        aspects: { type: array, items: { $ref: "#/components/schemas/AspectHit" } }
    SynastryRequest:
      type: object
      properties:
        a: { $ref: "#/components/schemas/NatalRequest" }
        b: { $ref: "#/components/schemas/NatalRequest" }
        orb: { type: number, description: "единый максимальный орб для всех аспектов" }
      required: [a, b]
    SynastryChart:
      type: object
      properties:
        a: { $ref: "#/components/schemas/NatalChart" }
        b: { $ref: "#/components/schemas/NatalChart" }
        aspects: { type: array, items: { $ref: "#/components/schemas/AspectHit" } }
        a_in_b_houses: { type: array, items: { $ref: "#/components/schemas/BodyInHouse" } }
        b_in_a_houses: { type: array, items: { $ref: "#/components/schemas/BodyInHouse" } }
        a_to_b_angles:
          type: array
          description: "кросс-аспекты: тела A к углам карты B (все четыре угла); пусто без домов B"
          items: { $ref: "#/components/schemas/AngleAspect" }
        b_to_a_angles:
          type: array
          description: "кросс-аспекты: тела B к углам карты A"
          items: { $ref: "#/components/schemas/AngleAspect" }
    SolarRequest:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalRequest" }
        year: { type: integer }
        location:
          oneOf: [{ $ref: "#/components/schemas/GeoLocation" }, { type: "null" }]
      required: [natal, year]
    SolarChart:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalChart" }
        solar: { $ref: "#/components/schemas/NatalChart" }
        aspects: { type: array, items: { $ref: "#/components/schemas/AspectHit" } }
    ExtrasResult:
      type: object
      properties:
        solar_eclipse:
          oneOf: [{ $ref: "#/components/schemas/EclipseInfo" }, { type: "null" }]
        lunar_eclipse:
          oneOf: [{ $ref: "#/components/schemas/EclipseInfo" }, { type: "null" }]
        algol_minima: { type: array, items: { type: string } }
        algol_longitude: { oneOf: [{ type: number }, { type: "null" }] }
    EclipseInfo:
      type: object
      properties:
        datetime_utc: { type: string }
        kind: { type: string }
    GlobalCalendar:
      type: object
      description: "небо года (все шесть блоков)"
      properties:
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        ingresses: { type: array, items: { $ref: "#/components/schemas/Ingress" } }
        stations: { type: array, items: { $ref: "#/components/schemas/Station" } }
        retrograde_periods: { type: array, items: { $ref: "#/components/schemas/RetrogradePeriod" } }
        transit_aspects: { type: array, items: { $ref: "#/components/schemas/TransitAspect" } }
        moon_phases: { type: array, items: { $ref: "#/components/schemas/MoonPhaseEvent" } }
        eclipses: { type: array, items: { $ref: "#/components/schemas/CalendarEclipse" } }
    Ingress:
      type: object
      description: "тело, входящее в знак (ретроградные повторные пересечения — отдельные записи)"
      properties:
        body: { $ref: "#/components/schemas/Body" }
        sign: { $ref: "#/components/schemas/Sign", description: "знак, в который вошло" }
        from_sign: { $ref: "#/components/schemas/Sign", description: "знак, который покинуло" }
        exact: { type: string, format: date-time }
        retrograde: { type: boolean, description: "направление через границу" }
    Station:
      type: object
      description: "ретроградная/директная станция (скорость меняет знак)"
      properties:
        body: { $ref: "#/components/schemas/Body" }
        kind: { type: string, enum: [retrograde, direct], description: "retrograde = D→R (начало ретро), direct = R→D (конец)" }
        exact: { type: string, format: date-time }
        longitude: { type: number }
        sign: { $ref: "#/components/schemas/Sign" }
    RetrogradePeriod:
      type: object
      description: "промежуток ретроградного движения (парные станции); обрезается по интервалу, если выходит за край"
      properties:
        body: { $ref: "#/components/schemas/Body" }
        start: { type: string, format: date-time }
        end: { type: string, format: date-time }
        sign_start: { $ref: "#/components/schemas/Sign" }
        sign_end: { $ref: "#/components/schemas/Sign" }
        clipped_start: { type: boolean, description: "начало — край интервала, а не настоящая станция" }
        clipped_end: { type: boolean, description: "конец — край интервала, а не настоящая станция" }
    TransitAspect:
      type: object
      description: "точный мунданный аспект между двумя движущимися телами"
      properties:
        body_a: { $ref: "#/components/schemas/Body" }
        body_b: { $ref: "#/components/schemas/Body" }
        kind: { type: string, enum: [conjunction, sextile, square, trine, opposition, quincunx] }
        angle: { type: number }
        exact: { type: string, format: date-time }
        longitude: { type: number, description: "долгота body_a в точный момент (якорь аспекта)" }
        retrograde_a: { type: boolean }
        retrograde_b: { type: boolean }
    MoonPhaseEvent:
      type: object
      description: "фаза Луны (элонгация пересекает 0/90/180/270°)"
      properties:
        phase: { type: string, enum: [new, first_quarter, full, last_quarter] }
        exact: { type: string, format: date-time }
        moon_sign: { $ref: "#/components/schemas/Sign" }
        sun_sign: { $ref: "#/components/schemas/Sign" }
        moon_longitude: { type: number }
        eclipse: { type: string, enum: [solar, lunar], nullable: true, description: "задано, если эта сизигия является затмением" }
    CalendarEclipse:
      type: object
      description: "затмение в интервале, с положением и узлом"
      properties:
        kind: { type: string, enum: [solar, lunar] }
        type: { type: string, enum: [total, annular, partial, penumbral, hybrid] }
        exact: { type: string, format: date-time }
        longitude: { type: number }
        sign: { $ref: "#/components/schemas/Sign" }
        node: { type: string, enum: [north, south] }
    UtcRequest:
      type: object
      properties:
        datetime_local: { type: string, example: "1990-05-15T12:00" }
        tz: { type: string, example: "Europe/Moscow" }
      required: [datetime_local, tz]
    UtcResult:
      type: object
      properties:
        datetime_utc: { type: string }
        offset: { type: string, example: "+04:00" }
        offset_seconds: { type: integer }
        abbreviation: { type: string }
    DeclAspect:
      type: object
      properties:
        a: { $ref: "#/components/schemas/Body" }
        b: { $ref: "#/components/schemas/Body" }
        kind: { type: string, enum: [parallel, contraparallel] }
        orb: { type: number }
    BodyInHouse:
      type: object
      properties:
        body: { $ref: "#/components/schemas/Body" }
        house: { type: integer, minimum: 1, maximum: 12 }
    ReturnRequest:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalRequest" }
        body: { $ref: "#/components/schemas/Body" }
        around: { type: string, format: date-time }
        location:
          oneOf: [{ $ref: "#/components/schemas/GeoLocation" }, { type: "null" }]
      required: [natal, body, around]
    ReturnChart:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalChart" }
        returned: { $ref: "#/components/schemas/NatalChart" }
        aspects: { type: array, items: { $ref: "#/components/schemas/AspectHit" } }
    ProgressedRequest:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalRequest" }
        target: { type: string, format: date-time }
        method: { type: string, enum: [secondary, symbolic], default: secondary }
        rate: { type: number, default: 1 }
      required: [natal, target]
    ProgressedChart:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalChart" }
        progressed: { $ref: "#/components/schemas/NatalChart" }
        aspects: { type: array, items: { $ref: "#/components/schemas/AspectHit" } }
        age_years: { type: number }
    PrimaryDirectionsRequest:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalRequest" }
        key: { type: string, enum: [naibod, ptolemy], default: naibod, description: "ключ дуга→время: naibod 0°59′08″/год или ptolemy 1°/год" }
        max_years: { type: number, default: 90, description: "верхняя граница возраста для событий" }
        converse: { type: boolean, default: true, description: "также считать конверсные дирекции" }
      required: [natal]
    PrimaryDirections:
      type: object
      properties:
        birth: { type: string, format: date-time }
        latitude: { type: number }
        armc: { type: number, description: "прямое восхождение MC (ARMC, градусы)" }
        obliquity: { type: number, description: "истинный наклон эклиптики (градусы)" }
        key: { type: string, enum: [naibod, ptolemy] }
        events:
          type: array
          items: { $ref: "#/components/schemas/DirectionEvent" }
          description: "перфекции в окне max_years, отсортированы по возрасту"
    DirectionEvent:
      type: object
      properties:
        significator: { type: string, enum: [ascendant, midheaven, sun, moon] }
        promissor: { $ref: "#/components/schemas/Body" }
        aspect: { type: string, enum: [conjunction, sextile, square, trine, opposition] }
        motion: { type: string, enum: [direct, converse], description: "direct — промиссор ведётся к сигнификатору; converse — наоборот" }
        arc: { type: number, description: "дуга дирекции в градусах прямого восхождения" }
        years: { type: number, description: "возраст перфекции (arc / ключ)" }
        date: { type: string, format: date-time, description: "дата перфекции (birth + years)" }
    AspectHitsRequest:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalRequest" }
        transit_body: { $ref: "#/components/schemas/Body", description: "одно транзитное тело; нужен он ИЛИ transit_bodies" }
        transit_bodies:
          type: array
          items: { $ref: "#/components/schemas/Body" }
          description: "несколько транзитных тел за один вызов (батч; натал считается один раз, дубликаты схлопываются); имеет приоритет над transit_body"
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        orb: { type: number, default: 1 }
        include_angles: { type: boolean, default: false, description: "также находить корни точных аспектов к углам карты (Asc/MC + зеркала Dsc/IC) → angle_hits" }
      required: [natal, from, to]
    AspectHits:
      type: object
      properties:
        orb: { type: number }
        hits:
          type: array
          items:
            type: object
            properties:
              transit_body: { $ref: "#/components/schemas/Body" }
              natal_body: { $ref: "#/components/schemas/Body" }
              kind: { type: string }
              angle: { type: number }
              exact: { type: string }
              enters: { type: string }
              leaves: { type: string }
        angle_hits:
          type: array
          description: "точные аспекты транзитного тела к углам карты (по запросу include_angles)"
          items:
            type: object
            properties:
              transit_body: { $ref: "#/components/schemas/Body" }
              angle: { type: string, enum: [ascendant, midheaven, descendant, imum_coeli] }
              kind: { type: string }
              exact_angle: { type: number }
              exact: { type: string }
              enters: { type: string }
              leaves: { type: string }
              derived: { type: boolean, description: "зеркало Dsc/IC; отсутствует, когда false" }
    MoonTransitRequest:
      type: object
      properties:
        natal: { $ref: "#/components/schemas/NatalRequest" }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time, description: "≤ 400 дней (~13 месяцев) после from" }
      required: [natal, from, to]
    MoonTransit:
      type: object
      properties:
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        void_windows:
          type: array
          items: { $ref: "#/components/schemas/MoonVoidWindow" }
        house_path:
          type: array
          description: "сегменты знак + натальный дом (только при натальной карте с указанным местом)"
          items: { $ref: "#/components/schemas/MoonPathSegment" }
    MoonVoidWindow:
      type: object
      description: "окно Луны без курса (Луна больше не образует аспектов до своей ингрессии)"
      properties:
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        via_combusta: { type: boolean, description: "отсутствует, когда false" }
        spica_exception: { type: boolean, description: "отсутствует, когда false" }
        clipped_start: { type: boolean, description: "уже открыто в начале интервала; отсутствует, когда false" }
        clipped_end: { type: boolean, description: "выходит за конец интервала; отсутствует, когда false" }
    MoonPathSegment:
      type: object
      description: "сегмент с постоянным знаком и натальным домом"
      properties:
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        sign: { $ref: "#/components/schemas/Sign" }
        natal_house: { type: integer, minimum: 1, maximum: 12 }
    GeoCity:
      type: object
      properties:
        name: { type: string }
        country: { type: string }
        admin1: { type: string }
        lat: { type: number }
        lon: { type: number }
        tz: { type: string }
        population: { type: integer }
