openapi: 3.1.0
info:
  title: 大势历命盘服务 API
  version: 1.6.0
  description: |
    Web、iOS、Android、桌面应用和游戏共用的四柱命盘、个人报告、合盘与台湾紫微斗数 REST 契约。
    所有确定性结果均绑定输入快照、数据源和引擎版本；LLM 只生成可选文案。
  contact:
    name: API Owner
servers:
  - url: http://127.0.0.1:8787
    description: Local development
  - url: https://api.example.com
    description: Production placeholder
tags:
  - name: System
  - name: Engine
  - name: Metadata
  - name: Localization
  - name: Places
  - name: Calendar
  - name: Profiles
  - name: Charts
  - name: Reports
  - name: Relationships
  - name: Compatibility
  - name: Ziwei
  - name: Ziwei Compatibility
  - name: Jobs
security:
  - bearerAuth: []
paths:
  /health:
    get:
      tags: [System]
      operationId: getHealth
      summary: 服务健康检查
      security: []
      responses:
        '200':
          description: 服务可用
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
  /v1/engine/capabilities:
    get:
      tags: [Engine]
      operationId: getEngineCapabilities
      summary: 获取无界面服务引擎的四项能力与版本
      security: []
      responses:
        '200':
          description: 当前可调用的功能、语言、计算档案和输出视图
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineCapabilities'
  /v1/engine/executions:
    post:
      tags: [Engine]
      operationId: executeEngine
      summary: 通过一个无状态入口执行个人盘、合盘、紫微个人盘或紫微双人合参
      description: |
        本接口不创建出生资料、命盘、关系或报告数据库资源。账户保存、导出与删除继续使用细粒度资源 API。
        相同输入和算法版本得到相同事实、claims 与 inputHash；执行 ID 和生成时间不参与事实哈希。
        reportMode=llm_enhanced 时只改写脱敏 claim 绑定的允许章节；上游或校验失败时仍以 200 返回完整模板和 fallback 元数据。
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EngineExecutionRequest'
      responses:
        '200':
          description: 无状态计算完成
          headers:
            X-Engine-Service-Version:
              schema: {type: string}
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineExecutionEnvelope'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/meta/source-manifest:
    get:
      tags: [Metadata]
      operationId: getActiveSourceManifest
      summary: 获取当前数据源与算法版本清单
      security: []
      responses:
        '200':
          description: 当前发布清单
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SourceManifest'
  /v1/meta/locales:
    get:
      tags: [Localization]
      operationId: listLocales
      summary: 获取内容语言、地区格式建议和解释档案发布状态
      security: []
      responses:
        '200':
          description: 可用、开发中和计划中的语言包
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/LocaleDefinition'
  /v1/meta/terminology:
    get:
      tags: [Localization]
      operationId: searchTerminology
      summary: 按稳定 concept ID 或目标语言检索术语
      security: []
      parameters:
        - name: contentLocale
          in: query
          required: true
          schema:
            $ref: '#/components/schemas/ContentLocale'
        - name: interpretationProfileId
          in: query
          required: true
          schema:
            type: string
            examples: [shichu-suimei-ja@1]
        - name: q
          in: query
          schema:
            type: string
            maxLength: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: 已发布术语
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TerminologyPage'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/places/search:
    get:
      tags: [Places]
      operationId: searchPlaces
      summary: 按简繁中文、日文、韩文、英文、拼音或行政区检索出生地
      security: []
      parameters:
        - name: q
          in: query
          schema:
            type: string
            minLength: 0
            maxLength: 100
        - name: countryCode
          in: query
          schema:
            type: string
            pattern: '^[A-Z]{2}$'
        - name: contentLocale
          in: query
          description: 控制地点名称展示，不影响地点匹配或时区
          schema:
            $ref: '#/components/schemas/ContentLocale'
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 10
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: 地点候选；客户端必须让用户确认歧义结果
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlaceSearchPage'
        '400':
          $ref: '#/components/responses/BadRequest'
  /v1/places/markets:
    get:
      tags: [Places]
      operationId: listBirthplaceMarkets
      summary: 读取出生地选择器支持的国家与地区
      security: []
      parameters:
        - name: contentLocale
          in: query
          schema:
            $ref: '#/components/schemas/ContentLocale'
      responses:
        '200':
          description: 国家／地区、可搜索地点数量及数据版本
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketPage'
  /v1/places/market-cities:
    get:
      tags: [Places]
      operationId: searchMarketCities
      summary: 在指定国家或地区内搜索可用于排盘的城市
      security: []
      parameters:
        - name: countryCode
          in: query
          required: true
          schema:
            type: string
            enum: [CN, TW, HK, MO, KR, JP, MY, TH, ID, SG, US, CA, GB, AU]
        - name: q
          in: query
          description: 可为空；为空时按人口与常用程度返回候选
          schema:
            type: string
            maxLength: 100
        - name: contentLocale
          in: query
          schema:
            $ref: '#/components/schemas/ContentLocale'
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: 带经纬度、IANA 时区及版本的城市候选
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketCityPage'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/places/china-divisions:
    get:
      tags: [Places]
      operationId: listChinaAdministrativeDivisions
      summary: 按省／市／县层级读取中国大陆行政区划及排盘代表点
      security: []
      parameters:
        - name: level
          in: query
          required: true
          schema:
            type: string
            enum: [province, city, county]
        - name: parentCode
          in: query
          description: 查询市或县时必填，分别传省级或市级代码
          schema:
            type: string
            pattern: '^\d{2,6}$'
      responses:
        '200':
          description: 版本化行政区划列表；县级条目包含可用于排盘的 placeId
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChinaDivisionPage'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/places/taiwan-divisions:
    get:
      deprecated: true
      tags: [Places]
      operationId: listTaiwanAdministrativeDivisions
      summary: 读取台湾 22 县市的排盘代表点
      description: 首发数据用于时区与真太阳时比较；临近斗数时辰边界时应提示用户确认更精确地点。
      security: []
      responses:
        '200':
          description: 台湾县市代表点与版本
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaiwanDivisionPage'
  /v1/calendar/convert:
    post:
      tags: [Calendar]
      operationId: convertCalendarDate
      summary: 公历与农历双向换算并返回节气边界信息
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CalendarConvertRequest'
      responses:
        '200':
          description: 换算结果
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CalendarConvertResult'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/time/calibrations:
    post:
      tags: [Calendar]
      operationId: calculateTimeCalibration
      summary: 计算历史时区、UTC、标准经度校正与真太阳时
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TimeCalibrationRequest'
      responses:
        '200':
          description: 校时结果与边界风险
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimeCalibration'
        '409':
          description: 地点或本地时间存在歧义，需要用户确认
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/birth-profiles:
    post:
      tags: [Profiles]
      operationId: createBirthProfile
      summary: 创建出生资料
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BirthProfileCreate'
      responses:
        '201':
          description: 已创建
          headers:
            Location:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirthProfile'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/birth-profiles/{profileId}:
    parameters:
      - $ref: '#/components/parameters/ProfileId'
    get:
      tags: [Profiles]
      operationId: getBirthProfile
      summary: 获取自己的出生资料
      responses:
        '200':
          description: 出生资料
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BirthProfile'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Profiles]
      operationId: deleteBirthProfile
      summary: 删除出生资料并安排清除衍生数据
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: 删除任务已接收，数据立即对业务查询不可见
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/charts:
    post:
      tags: [Charts]
      operationId: createChart
      summary: 基于出生资料生成不可变个人命盘快照
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChartCreateRequest'
      responses:
        '201':
          description: 命盘已生成
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChartSnapshot'
        '409':
          description: 临界口径尚未确认或幂等键冲突
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/charts/{chartId}:
    get:
      tags: [Charts]
      operationId: getChart
      summary: 获取命盘快照
      parameters:
        - $ref: '#/components/parameters/ChartId'
      responses:
        '200':
          description: 结构化命盘
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChartSnapshot'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/charts/{chartId}/evidence:
    get:
      tags: [Charts]
      operationId: getChartEvidence
      summary: 获取规则命中、claim 与证据链
      parameters:
        - $ref: '#/components/parameters/ChartId'
      responses:
        '200':
          description: 可审计证据链
          content:
            application/json:
              schema:
                type: object
                required: [chartId, claims]
                properties:
                  chartId:
                    type: string
                    format: uuid
                  claims:
                    type: array
                    items:
                      $ref: '#/components/schemas/Claim'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/reports:
    post:
      tags: [Reports]
      operationId: createPersonalReport
      summary: 生成个人命盘报告
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportCreateRequest'
      responses:
        '201':
          description: 固定模板报告已同步生成
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Report'
        '202':
          description: LLM 增强报告已进入异步任务
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/reports/{reportId}:
    get:
      tags: [Reports]
      operationId: getReport
      summary: 获取个人或合盘报告
      parameters:
        - $ref: '#/components/parameters/ReportId'
      responses:
        '200':
          description: 报告
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Report'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/reports/{reportId}/exports:
    post:
      tags: [Reports]
      operationId: createReportExport
      summary: 创建 Markdown、HTML 或 PDF 导出
      parameters:
        - $ref: '#/components/parameters/ReportId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExportCreateRequest'
      responses:
        '202':
          description: 导出任务已接收
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/ziwei/charts:
    post:
      tags: [Ziwei]
      operationId: createZiweiChart
      summary: 建立独立的台湾紫微斗数命盘快照
      description: 不会创建四柱个人报告；斗数农历月与四柱节气月分别保存。
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZiweiChartCreateRequest'
      responses:
        '201':
          description: 紫微命盘快照已生成
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZiweiChartSnapshot'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/ziwei/charts/{ziweiChartId}:
    get:
      tags: [Ziwei]
      operationId: getZiweiChart
      summary: 获取紫微斗数命盘快照
      parameters:
        - $ref: '#/components/parameters/ZiweiChartId'
      responses:
        '200':
          description: 十二宫、星曜、四化及结构事实
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZiweiChartSnapshot'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/ziwei/reports:
    post:
      tags: [Ziwei]
      operationId: createZiweiReport
      summary: 生成台湾繁体紫微斗数报告
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZiweiReportCreateRequest'
      responses:
        '201':
          description: 固定模板报告已生成
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZiweiReport'
        '202':
          description: 可选 LLM 增强任务已接收；未配置时安全回退固定模板
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/ziwei/reports/{ziweiReportId}:
    get:
      tags: [Ziwei]
      operationId: getZiweiReport
      summary: 获取紫微斗数报告
      parameters:
        - $ref: '#/components/parameters/ZiweiReportId'
      responses:
        '200':
          description: 台湾繁体紫微斗数报告
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZiweiReport'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/ziwei/reports/{ziweiReportId}/exports:
    post:
      tags: [Ziwei]
      operationId: createZiweiReportExport
      summary: 创建紫微报告 Markdown、HTML 或 PDF 导出
      parameters:
        - $ref: '#/components/parameters/ZiweiReportId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExportCreateRequest'
      responses:
        '202':
          description: 导出任务已接收
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/relationships:
    post:
      tags: [Relationships]
      operationId: createRelationship
      summary: 建立双方关系及授权状态
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RelationshipCreateRequest'
      responses:
        '201':
          description: 关系已创建；未完成双方授权时不可生成合盘
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Relationship'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/relationships/{relationshipId}/consents:
    post:
      tags: [Relationships]
      operationId: recordRelationshipConsent
      summary: 授予或撤回合盘使用授权
      parameters:
        - $ref: '#/components/parameters/RelationshipId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConsentRequest'
      responses:
        '200':
          description: 授权状态已更新
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Relationship'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/compatibility-reports:
    post:
      tags: [Compatibility]
      operationId: createCompatibilityReport
      summary: 独立生成合盘报告，可直接提交双方资料或复用已有命盘
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompatibilityReportCreateRequest'
      responses:
        '201':
          description: 固定版合盘报告已生成
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Report'
        '202':
          description: LLM 增强版已进入异步任务
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
        '403':
          description: 资料使用授权无效、未完成或已撤回
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          description: 复用快照不兼容，或新填成员需要确认时间口径
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/ziwei-compatibility-reports:
    post:
      tags: [Ziwei Compatibility]
      operationId: createZiweiCompatibilityReport
      summary: 独立生成台湾紫微斗数夫妻／情侣双人合参报告
      description: 直接提交双方资料或复用紫微命盘；后台先建立两张独立命盘快照，但不自动生成个人报告。首版固定三合结构与生年四化双向落宫，不输出匹配百分比。
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZiweiCompatibilityReportCreateRequest'
      responses:
        '201':
          description: 16 章繁体双人合参报告已生成
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZiweiCompatibilityReport'
        '202':
          description: 可选 LLM 增强任务已接收；未配置时安全回退固定模板
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
        '422':
          $ref: '#/components/responses/Unprocessable'
  /v1/ziwei-compatibility-reports/{ziweiCompatibilityReportId}:
    get:
      tags: [Ziwei Compatibility]
      operationId: getZiweiCompatibilityReport
      summary: 获取紫微斗数双人合参报告
      parameters:
        - $ref: '#/components/parameters/ZiweiCompatibilityReportId'
      responses:
        '200':
          description: 台湾繁体双人合参报告
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZiweiCompatibilityReport'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/ziwei-compatibility-reports/{ziweiCompatibilityReportId}/exports:
    post:
      tags: [Ziwei Compatibility]
      operationId: createZiweiCompatibilityReportExport
      summary: 创建紫微双人合参报告 Markdown、HTML 或 PDF 导出
      parameters:
        - $ref: '#/components/parameters/ZiweiCompatibilityReportId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExportCreateRequest'
      responses:
        '202':
          description: 导出任务已接收
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/jobs/{jobId}:
    get:
      tags: [Jobs]
      operationId: getJob
      summary: 查询异步任务状态
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: 任务状态
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 由 OIDC 身份服务签发的访问令牌
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: 客户端生成的 UUID；同一用户、同一操作保留 24 小时
      schema:
        type: string
        format: uuid
    ProfileId:
      name: profileId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ChartId:
      name: chartId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ZiweiChartId:
      name: ziweiChartId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ReportId:
      name: reportId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ZiweiReportId:
      name: ziweiReportId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ZiweiCompatibilityReportId:
      name: ziweiCompatibilityReportId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    RelationshipId:
      name: relationshipId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    JobId:
      name: jobId
      in: path
      required: true
      schema:
        type: string
        format: uuid
  responses:
    BadRequest:
      description: 请求格式错误
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: 资源不存在或当前用户无权查看
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Conflict:
      description: 资源状态或幂等键冲突
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unprocessable:
      description: 输入结构合法但无法按业务规则处理
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    Health:
      type: object
      additionalProperties: false
      required: [status, version, serviceEngineVersion, time]
      properties:
        status:
          type: string
          enum: [ok, degraded]
        version:
          type: string
        serviceEngineVersion:
          type: string
          examples: [dashilee-service-engine@1.1.0]
        time:
          type: string
          format: date-time
    EngineServiceDescriptor:
      type: object
      additionalProperties: false
      required: [id, version, apiVersion, stateMode]
      properties:
        id:
          type: string
          const: dashilee-service-engine
        version:
          type: string
          examples: [dashilee-service-engine@1.1.0]
        apiVersion:
          type: string
          const: '1'
        stateMode:
          type: string
          const: ephemeral
    EngineFeature:
      type: string
      enum: [bazi.personal, bazi.compatibility, ziwei.personal, ziwei.compatibility]
    EngineFeatureDefinition:
      type: object
      additionalProperties: false
      required: [id, label, subjectCount, contentLocales, calculationProfiles, reportSections]
      properties:
        id:
          $ref: '#/components/schemas/EngineFeature'
        label: {type: string}
        subjectCount:
          type: integer
          enum: [1, 2]
        contentLocales:
          type: array
          items: {type: string}
        calculationProfiles:
          type: array
          items: {type: string}
        reportSections:
          type: object
          additionalProperties:
            type: integer
        requiresConsent:
          type: boolean
          default: false
    EngineCapabilities:
      type: object
      additionalProperties: false
      required: [service, transport, executionEndpoint, responseViews, stateModes, deterministicCore, llmRequired, llm, features, limits]
      properties:
        service:
          $ref: '#/components/schemas/EngineServiceDescriptor'
        transport:
          type: object
          additionalProperties: false
          required: [protocol, mediaType, contract]
          properties:
            protocol: {type: string, const: https}
            mediaType: {type: string, const: application/json}
            contract: {type: string, const: OpenAPI 3.1}
        executionEndpoint: {type: string, const: /v1/engine/executions}
        responseViews:
          type: array
          items:
            type: string
            enum: [facts, report, full]
        stateModes:
          type: array
          items:
            type: string
            const: ephemeral
        deterministicCore: {type: boolean, const: true}
        llmRequired: {type: boolean, const: false}
        llm:
          $ref: '#/components/schemas/LlmCapability'
        features:
          type: array
          minItems: 4
          maxItems: 4
          items:
            $ref: '#/components/schemas/EngineFeatureDefinition'
        limits:
          type: object
          additionalProperties: false
          required: [requestBodyBytes, supportedDateRange]
          properties:
            requestBodyBytes: {type: integer}
            supportedDateRange:
              type: array
              minItems: 2
              maxItems: 2
              items: {type: integer}
    LlmCapability:
      type: object
      additionalProperties: false
      required: [enabled, configured, provider, policyVersion, promptVersion, defaultModels, configSource, configurationError, role, fallback]
      properties:
        enabled: {type: boolean}
        configured: {type: boolean}
        provider: {type: [string, 'null']}
        policyVersion: {type: [string, 'null']}
        promptVersion: {type: [string, 'null']}
        defaultModels:
          type: array
          items: {type: string}
        configSource:
          type: string
          enum: [defaults, file, invalid]
        configurationError: {type: [string, 'null']}
        role:
          type: string
          const: optional_constrained_report_rewrite
        fallback:
          type: string
          const: deterministic_template
    EngineSubjectInput:
      type: object
      additionalProperties: false
      required: [birthData]
      properties:
        birthData:
          $ref: '#/components/schemas/BirthProfileCreate'
        timeBasis:
          type: string
          enum: [civil, true_solar]
          description: 四柱默认 true_solar；紫微默认 civil
        calculationProfileId:
          type: string
          description: 紫微首发仅支持 ziwei-tw-sanhe@1
    EnginePersonalInput:
      type: object
      additionalProperties: false
      required: [subject]
      properties:
        subject:
          $ref: '#/components/schemas/EngineSubjectInput'
    EngineCompatibilityInput:
      type: object
      additionalProperties: false
      required: [memberA, memberB, relationshipType, consent]
      properties:
        memberA:
          $ref: '#/components/schemas/EngineSubjectInput'
        memberB:
          $ref: '#/components/schemas/EngineSubjectInput'
        relationshipType:
          type: string
          enum: [married, partners, dating, other]
        consent:
          type: object
          additionalProperties: true
          required: [confirmed]
          properties:
            confirmed: {type: boolean, const: true}
            basis: {type: string}
        referenceYear:
          type: integer
          minimum: 1800
          maximum: 2200
    EngineExecutionOptions:
      type: object
      additionalProperties: false
      properties:
        responseView:
          type: string
          enum: [facts, report, full]
          default: full
        reportMode:
          type: string
          enum: [template, llm_enhanced]
          default: template
          description: llm_enhanced 只增强报告文案；任何失败均返回完整确定性模板
        contentLocale: {type: string}
        formatLocale: {type: string}
        interpretationProfileId: {type: string}
        reportReferenceAt:
          type: string
          format: date-time
    EngineExecutionRequest:
      type: object
      additionalProperties: false
      required: [feature, input]
      properties:
        feature:
          $ref: '#/components/schemas/EngineFeature'
        input:
          oneOf:
            - $ref: '#/components/schemas/EnginePersonalInput'
            - $ref: '#/components/schemas/EngineCompatibilityInput'
        options:
          $ref: '#/components/schemas/EngineExecutionOptions'
    EngineSubject:
      type: object
      additionalProperties: false
      required: [role, displayName, gender, timePrecision, timeBasis, localDateTime, place]
      properties:
        role:
          type: string
          enum: [subject, memberA, memberB]
        displayName: {type: [string, 'null']}
        gender:
          type: string
          enum: [male, female]
        timePrecision: {type: string}
        timeBasis:
          type: string
          enum: [civil, true_solar]
        localDateTime:
          type: object
          additionalProperties: true
        place:
          type: object
          additionalProperties: true
    EngineChart:
      type: object
      additionalProperties: true
      required: [role, method, timeBasis, normalizedInputHash, facts, warnings, versions]
      properties:
        role: {type: string}
        method: {type: string}
        timeBasis: {type: string}
        normalizedInputHash: {type: string}
        chartHash: {type: [string, 'null']}
        pillars:
          type: [array, 'null']
          items:
            type: object
            additionalProperties: true
        facts:
          type: object
          additionalProperties: true
        warnings:
          type: array
          items: {type: string}
        versions:
          type: object
          additionalProperties: true
    EngineClaim:
      type: object
      additionalProperties: false
      required: [id, code, origin, topic, statement, perspective, confidence, evidence, limitations, ruleIds]
      properties:
        id: {type: string}
        code: {type: string}
        origin:
          type: string
          enum: [subject, memberA, memberB, compatibility]
        topic: {type: string}
        statement: {type: string}
        perspective: {type: [string, 'null']}
        confidence: {type: string}
        evidence:
          type: array
          items: {}
        limitations:
          type: array
          items: {type: string}
        ruleIds:
          type: array
          items: {type: string}
    EngineExecutionEnvelope:
      type: object
      additionalProperties: false
      required: [service, execution, result, warnings, versions]
      properties:
        service:
          $ref: '#/components/schemas/EngineServiceDescriptor'
        execution:
          type: object
          additionalProperties: false
          required: [id, feature, stateMode, responseView, status, inputHash, generatedAt]
          properties:
            id: {type: string}
            feature:
              $ref: '#/components/schemas/EngineFeature'
            stateMode: {type: string, const: ephemeral}
            responseView:
              type: string
              enum: [facts, report, full]
            status: {type: string, const: succeeded}
            inputHash: {type: string}
            generatedAt: {type: string, format: date-time}
        result:
          type: object
          additionalProperties: false
          required: [subjects]
          properties:
            subjects:
              type: array
              minItems: 1
              maxItems: 2
              items:
                $ref: '#/components/schemas/EngineSubject'
            charts:
              type: array
              items:
                $ref: '#/components/schemas/EngineChart'
            analysis:
              type: [object, 'null']
              additionalProperties: true
            claims:
              type: array
              items:
                $ref: '#/components/schemas/EngineClaim'
            report:
              type: object
              additionalProperties: true
        warnings:
          type: array
          items: {type: string}
        versions:
          type: object
          additionalProperties: true
    SourceManifest:
      type: object
      additionalProperties: false
      required: [id, status, publishedAt, sources, policyVersions, contentReleases]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [draft, active, retired]
        publishedAt:
          type: string
          format: date-time
        sources:
          type: array
          items:
            type: object
            required: [name, version, license, checksum]
            properties:
              name: {type: string}
              version: {type: string}
              license: {type: string}
              checksum: {type: string}
        policyVersions:
          type: object
          additionalProperties:
            type: string
        contentReleases:
          type: object
          additionalProperties:
            type: string
          examples:
            - zh-Hans: zh-Hans@1.0.0
              zh-Hant: zh-Hant@1.0.0
              ja: ja@1.0.0
              ko: ko@1.0.0
    ContentLocale:
      type: string
      description: 内容语言；英文可在结构中出现，但未发布时报告接口返回 LOCALE_PACK_NOT_RELEASED
      enum: [zh-Hans, zh-Hant, ja, ko, en]
    LocaleDefinition:
      type: object
      additionalProperties: false
      required:
        - contentLocale
        - nativeName
        - defaultFormatLocale
        - status
        - interpretationProfiles
      properties:
        contentLocale:
          $ref: '#/components/schemas/ContentLocale'
        nativeName: {type: string}
        defaultFormatLocale:
          type: string
          examples: [ja-JP]
        acceptedFormatLocales:
          type: array
          items: {type: string}
        status:
          type: string
          enum: [planned, development, active, retired]
        releaseId: {type: [string, 'null']}
        interpretationProfiles:
          type: array
          items:
            $ref: '#/components/schemas/InterpretationProfileSummary'
    InterpretationProfileSummary:
      type: object
      additionalProperties: false
      required: [id, contentLocale, displayName, status]
      properties:
        id:
          type: string
          examples: [saju-myeongri-ko@1]
        contentLocale:
          $ref: '#/components/schemas/ContentLocale'
        displayName: {type: string}
        status:
          type: string
          enum: [planned, development, active, retired]
        compatibleCalculationPolicyIds:
          type: array
          items: {type: string}
    TerminologyEntry:
      type: object
      additionalProperties: false
      required: [conceptId, contentLocale, interpretationProfileId, term, reviewStatus, releaseId]
      properties:
        conceptId:
          type: string
          examples: [ten_god.direct_officer]
        contentLocale:
          $ref: '#/components/schemas/ContentLocale'
        interpretationProfileId: {type: string}
        term: {type: string}
        shortTerm: {type: [string, 'null']}
        reading: {type: [string, 'null']}
        romanization: {type: [string, 'null']}
        explanation: {type: string}
        reviewStatus:
          type: string
          enum: [reviewed]
        releaseId: {type: string}
    TerminologyPage:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/TerminologyEntry'
        nextCursor: {type: [string, 'null']}
    PlaceSummary:
      type: object
      additionalProperties: false
      required: [id, displayName, canonicalName, countryCode, latitude, longitude, timeZoneId, datasetVersion]
      properties:
        id: {type: string}
        displayName: {type: string}
        canonicalName: {type: string}
        countryCode:
          type: string
          pattern: '^[A-Z]{2}$'
        admin1: {type: [string, 'null']}
        admin2: {type: [string, 'null']}
        latitude: {type: number, minimum: -90, maximum: 90}
        longitude: {type: number, minimum: -180, maximum: 180}
        timeZoneId: {type: string, examples: [Asia/Shanghai]}
        standardOffsetMinutes: {type: [integer, 'null']}
        datasetVersion: {type: string}
        matchReason: {type: [string, 'null']}
    PlaceSearchPage:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PlaceSummary'
        nextCursor: {type: [string, 'null']}
    MarketSummary:
      type: object
      additionalProperties: false
      required: [countryCode, displayName, placeCount, datasetVersion]
      properties:
        countryCode:
          type: string
          pattern: '^[A-Z]{2}$'
        displayName: {type: string}
        placeCount: {type: integer, minimum: 1}
        datasetVersion: {type: [string, 'null']}
    MarketPage:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/MarketSummary'
    MarketCityPage:
      type: object
      additionalProperties: false
      required: [items, countryCode]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PlaceSummary'
        nextCursor: {type: [string, 'null']}
        countryCode:
          type: string
          pattern: '^[A-Z]{2}$'
    ChinaDivisionSummary:
      type: object
      additionalProperties: false
      required: [code, level, name, fullName, latitude, longitude, coordinateAccuracy, datasetVersion]
      properties:
        code: {type: string}
        parentCode: {type: [string, 'null']}
        level:
          type: string
          enum: [province, city, county]
        name: {type: string}
        fullName: {type: string}
        pinyin: {type: [string, 'null']}
        placeId: {type: [string, 'null']}
        latitude: {type: number, minimum: -90, maximum: 90}
        longitude: {type: number, minimum: -180, maximum: 180}
        coordinateAccuracy:
          type: string
          enum: [province_centroid, city_centroid, county_centroid, parent_centroid]
        datasetVersion: {type: string}
    ChinaDivisionPage:
      type: object
      additionalProperties: false
      required: [items, datasetVersion]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ChinaDivisionSummary'
        nextCursor: {type: [string, 'null']}
        datasetVersion: {type: [string, 'null']}
    TaiwanDivisionPage:
      type: object
      additionalProperties: false
      required: [items, datasetVersion]
      properties:
        items:
          type: array
          minItems: 22
          maxItems: 22
          items:
            $ref: '#/components/schemas/PlaceSummary'
        datasetVersion:
          type: string
          const: tw-admin-centers@2026-09-01
    CalendarDateInput:
      type: object
      additionalProperties: false
      required: [calendar, year, month, day]
      properties:
        calendar:
          type: string
          enum: [gregorian, lunar]
        year: {type: integer, minimum: 1800, maximum: 2200}
        month: {type: integer, minimum: 1, maximum: 12}
        day: {type: integer, minimum: 1, maximum: 31}
        isLeapMonth:
          type: boolean
          default: false
    CalendarConvertRequest:
      type: object
      additionalProperties: false
      required: [date]
      properties:
        date:
          $ref: '#/components/schemas/CalendarDateInput'
        timeZoneId:
          type: string
          default: Asia/Shanghai
    CalendarConvertResult:
      type: object
      additionalProperties: false
      required: [gregorian, lunar, nearbySolarTerms, sourceManifestId]
      properties:
        gregorian:
          type: object
          required: [year, month, day]
          properties:
            year: {type: integer}
            month: {type: integer}
            day: {type: integer}
        lunar:
          type: object
          required: [year, month, day, isLeapMonth]
          properties:
            year: {type: integer}
            month: {type: integer}
            day: {type: integer}
            isLeapMonth: {type: boolean}
        nearbySolarTerms:
          type: array
          items:
            type: object
            required: [name, instantUtc]
            properties:
              name: {type: string}
              instantUtc: {type: string, format: date-time}
        sourceManifestId: {type: string}
    LocalDateTimeInput:
      type: object
      additionalProperties: false
      required: [date, time]
      properties:
        date:
          $ref: '#/components/schemas/CalendarDateInput'
        time:
          type: string
          pattern: '^([01][0-9]|2[0-3]):[0-5][0-9]$'
        fold:
          type: integer
          enum: [0, 1]
          description: 夏令时回拨产生两个相同本地时刻时选择第一个或第二个
    TimeCalibrationRequest:
      type: object
      additionalProperties: false
      required: [localDateTime, placeId]
      properties:
        localDateTime:
          $ref: '#/components/schemas/LocalDateTimeInput'
        placeId: {type: string}
        compareTrueSolarTime:
          type: boolean
          default: true
    TimeCalibration:
      type: object
      additionalProperties: false
      required:
        - civilTime
        - utcTime
        - timeZoneId
        - utcOffsetSeconds
        - daylightSavingSeconds
        - trueSolarTime
        - equationOfTimeSeconds
        - longitudeCorrectionSeconds
        - boundaryRisks
        - sourceManifestId
      properties:
        civilTime: {type: string, format: date-time}
        utcTime: {type: string, format: date-time}
        timeZoneId: {type: string}
        utcOffsetSeconds: {type: integer}
        daylightSavingSeconds: {type: integer}
        trueSolarTime: {type: string, format: date-time}
        equationOfTimeSeconds: {type: integer}
        longitudeCorrectionSeconds: {type: integer}
        crossesHourPillarBoundary: {type: boolean}
        crossesDayBoundary: {type: boolean}
        boundaryRisks:
          type: array
          items:
            type: string
            enum: [dst_gap, dst_fold, solar_term, hour_pillar, day_pillar, year_pillar]
        sourceManifestId: {type: string}
    BirthProfileCreate:
      type: object
      additionalProperties: false
      required: [gender, localDateTime, timePrecision, placeId, calendarPolicy]
      properties:
        displayName:
          type: [string, 'null']
          maxLength: 80
        gender:
          type: string
          enum: [male, female]
        localDateTime:
          $ref: '#/components/schemas/LocalDateTimeInput'
        timePrecision:
          type: string
          enum: [minute, double_hour, range, unknown]
        timeRangeEnd:
          type: [string, 'null']
          pattern: '^([01][0-9]|2[0-3]):[0-5][0-9]$'
        placeId: {type: string}
        calendarPolicy:
          type: object
          additionalProperties: false
          required: [yearBoundary, monthBoundary, dayBoundary]
          properties:
            yearBoundary:
              type: string
              enum: [li_chun]
            monthBoundary:
              type: string
              enum: [jie]
            dayBoundary:
              type: string
              enum: [zi_early_23]
        notes:
          type: [string, 'null']
          maxLength: 500
    BirthProfile:
      allOf:
        - $ref: '#/components/schemas/BirthProfileCreate'
        - type: object
          required: [id, ownerAccountId, createdAt, updatedAt, status]
          properties:
            id: {type: string, format: uuid}
            ownerAccountId: {type: string}
            status:
              type: string
              enum: [active, deleting]
            createdAt: {type: string, format: date-time}
            updatedAt: {type: string, format: date-time}
    ChartCreateRequest:
      type: object
      additionalProperties: false
      required: [birthProfileId, timeBasis]
      properties:
        birthProfileId: {type: string, format: uuid}
        timeBasis:
          type: string
          enum: [civil, true_solar]
        calibrationConfirmationId:
          type: [string, 'null']
          format: uuid
        includeLuckCycles:
          type: boolean
          default: true
    ZiweiChartCreateRequest:
      type: object
      additionalProperties: false
      required: [birthProfileId, timeBasis]
      properties:
        birthProfileId: {type: string, format: uuid}
        timeBasis:
          type: string
          enum: [civil, true_solar]
        calculationProfileId:
          type: string
          const: ziwei-tw-sanhe@1
          default: ziwei-tw-sanhe@1
    ZiweiChartSnapshot:
      type: object
      additionalProperties: false
      required: [id, birthProfileId, method, timeBasis, normalizedInputHash, chartHash, facts, claims, warnings, versions, createdAt]
      properties:
        id: {type: string, format: uuid}
        birthProfileId: {type: string, format: uuid}
        method:
          type: string
          const: ziwei
        timeBasis:
          type: string
          enum: [civil, true_solar]
        normalizedInputHash: {type: string}
        chartHash: {type: string}
        facts:
          type: object
          description: 版本化十二宫、星曜、生年四化与结构事实；斗数农历字段不得覆盖四柱节气月字段
          additionalProperties: true
        claims:
          type: array
          items:
            type: object
            additionalProperties: true
        warnings:
          type: array
          items: {type: string}
        versions:
          $ref: '#/components/schemas/VersionBundle'
        createdAt: {type: string, format: date-time}
    Pillar:
      type: object
      additionalProperties: false
      required: [position, stem, branch]
      properties:
        position:
          type: string
          enum: [year, month, day, hour]
        stem: {type: string, minLength: 1, maxLength: 1}
        branch: {type: string, minLength: 1, maxLength: 1}
        tenGod: {type: [string, 'null']}
        hiddenStems:
          type: array
          items:
            type: object
            required: [stem, weight]
            properties:
              stem: {type: string}
              tenGod: {type: [string, 'null']}
              weight: {type: number, minimum: 0, maximum: 1}
    ChartSnapshot:
      type: object
      additionalProperties: false
      required:
        - id
        - birthProfileId
        - genderLabel
        - timeBasis
        - normalizedInputHash
        - pillars
        - facts
        - versions
        - createdAt
      properties:
        id: {type: string, format: uuid}
        birthProfileId: {type: string, format: uuid}
        genderLabel:
          type: string
          enum: [乾造, 坤造]
        timeBasis:
          type: string
          enum: [civil, true_solar]
        normalizedInputHash: {type: string}
        pillars:
          type: array
          minItems: 3
          maxItems: 4
          items:
            $ref: '#/components/schemas/Pillar'
        facts:
          type: object
          description: 版本化确定性事实；具体字段由 bazi-core schema 管理
          additionalProperties: true
        warnings:
          type: array
          items: {type: string}
        versions:
          $ref: '#/components/schemas/VersionBundle'
        createdAt: {type: string, format: date-time}
    VersionBundle:
      type: object
      additionalProperties: false
      required:
        - engineVersion
        - calendarPolicyVersion
        - tzdbVersion
        - placeDatasetVersion
        - astronomyVersion
        - sourceManifestId
      properties:
        engineVersion: {type: string}
        calendarPolicyVersion: {type: string}
        tzdbVersion: {type: string}
        placeDatasetVersion: {type: string}
        astronomyVersion: {type: string}
        sourceManifestId: {type: string}
        rulePackVersion: {type: [string, 'null']}
        templateVersion: {type: [string, 'null']}
    Claim:
      type: object
      additionalProperties: false
      required: [id, topic, statement, confidence, evidence, limitations, ruleIds]
      properties:
        id: {type: string, format: uuid}
        topic: {type: string}
        statement: {type: string}
        confidence:
          type: string
          enum: [high, medium, low, conditional]
        evidence:
          type: array
          items:
            type: object
            required: [path, value]
            properties:
              path: {type: string, examples: [pillars.day.stem]}
              value: {}
        limitations:
          type: array
          items: {type: string}
        ruleIds:
          type: array
          items: {type: string}
    ReportCreateRequest:
      type: object
      additionalProperties: false
      required: [chartId, mode, contentLocale, formatLocale, interpretationProfileId]
      properties:
        chartId: {type: string, format: uuid}
        mode:
          type: string
          enum: [template, llm_enhanced]
        contentLocale:
          $ref: '#/components/schemas/ContentLocale'
        formatLocale:
          type: string
          minLength: 2
          maxLength: 35
          examples: [zh-Hant-HK]
        interpretationProfileId:
          type: string
          examples: [ziping-zh-Hans@1]
        style:
          type: string
          enum: [concise, balanced, detailed]
          default: balanced
        reportReferenceAt:
          type: string
          format: date-time
          description: 报告解释“当前大运、流年和流月”所使用的参考时刻；省略时取服务端当前时刻
    ZiweiReportCreateRequest:
      type: object
      additionalProperties: false
      required: [ziweiChartId, mode, contentLocale, formatLocale, interpretationProfileId]
      properties:
        ziweiChartId: {type: string, format: uuid}
        mode:
          type: string
          enum: [template, llm_enhanced]
        contentLocale:
          type: string
          const: zh-Hant
        formatLocale:
          type: string
          const: zh-Hant-TW
        interpretationProfileId:
          type: string
          const: ziwei-tw-sanhe@1
        reportReferenceAt:
          type: string
          format: date-time
          description: 当前大限与流年的参考时刻；省略时取服务端当前时刻
    ZiweiCompatibilityReportCreateRequest:
      type: object
      additionalProperties: false
      required: [memberA, memberB, relationshipType, consent, mode, contentLocale, formatLocale, interpretationProfileId]
      properties:
        relationshipType:
          type: string
          enum: [married, partners, dating, other]
        memberA:
          $ref: '#/components/schemas/ZiweiCompatibilityMemberInput'
        memberB:
          $ref: '#/components/schemas/ZiweiCompatibilityMemberInput'
        consent:
          $ref: '#/components/schemas/CompatibilityConsentAttestation'
        mode:
          type: string
          enum: [template, llm_enhanced]
        contentLocale:
          type: string
          const: zh-Hant
        formatLocale:
          type: string
          const: zh-Hant-TW
        interpretationProfileId:
          type: string
          const: ziwei-tw-compat-sanhe@1
        reportReferenceAt:
          type: string
          format: date-time
          description: 双方大限、流年与六年时间轴的参考时刻
    ZiweiCompatibilityMemberInput:
      oneOf:
        - $ref: '#/components/schemas/ExistingZiweiCompatibilityMember'
        - $ref: '#/components/schemas/InlineZiweiCompatibilityMember'
      discriminator:
        propertyName: sourceType
        mapping:
          existing_ziwei_chart: '#/components/schemas/ExistingZiweiCompatibilityMember'
          inline_birth: '#/components/schemas/InlineZiweiCompatibilityMember'
    ExistingZiweiCompatibilityMember:
      type: object
      additionalProperties: false
      required: [sourceType, ziweiChartId]
      properties:
        sourceType:
          type: string
          const: existing_ziwei_chart
        ziweiChartId: {type: string, format: uuid}
        displayLabel: {type: [string, 'null'], maxLength: 80}
    InlineZiweiCompatibilityMember:
      type: object
      additionalProperties: false
      required: [sourceType, birthData, timeBasis]
      properties:
        sourceType:
          type: string
          const: inline_birth
        birthData:
          $ref: '#/components/schemas/BirthProfileCreate'
        timeBasis:
          type: string
          enum: [civil, true_solar]
        calculationProfileId:
          type: string
          const: ziwei-tw-sanhe@1
          default: ziwei-tw-sanhe@1
        saveAsPersonalProfile:
          type: boolean
          default: false
          description: 默认只保存合参内部依赖；true 只保留个人命盘资料，仍不自动生成个人报告
    CompatibilityReportCreateRequest:
      type: object
      additionalProperties: false
      required:
        - memberA
        - memberB
        - relationshipType
        - consent
        - mode
        - contentLocale
        - formatLocale
        - interpretationProfileId
      properties:
        relationshipId:
          type: [string, 'null']
          format: uuid
          description: 可选；仅复用已保存关系或邀请授权流程时提供
        relationshipType:
          type: string
          enum: [married, partners, dating, other]
        memberA:
          $ref: '#/components/schemas/CompatibilityMemberInput'
        memberB:
          $ref: '#/components/schemas/CompatibilityMemberInput'
        consent:
          $ref: '#/components/schemas/CompatibilityConsentAttestation'
        mode:
          type: string
          enum: [template, llm_enhanced]
        contentLocale:
          $ref: '#/components/schemas/ContentLocale'
        formatLocale:
          type: string
          minLength: 2
          maxLength: 35
          examples: [ko-KR]
        interpretationProfileId:
          type: string
          examples: [saju-myeongri-ko@1]
        includeJointTimeline:
          type: boolean
          default: true
        referenceYear:
          type: integer
          minimum: 1800
          maximum: 2200
          description: 合盘关系时间轴的起始公历年；省略时由 reportReferenceAt 推导
        reportReferenceAt:
          type: string
          format: date-time
          description: 双方当前大运与共同流年的报告参考时刻；若仅提供 referenceYear，则默认使用该年 1 月 1 日
    CompatibilityMemberInput:
      description: 每位成员可以复用已有命盘，或在合盘入口直接填写出生资料
      oneOf:
        - $ref: '#/components/schemas/ExistingChartCompatibilityMember'
        - $ref: '#/components/schemas/InlineBirthCompatibilityMember'
      discriminator:
        propertyName: sourceType
        mapping:
          existing_chart: '#/components/schemas/ExistingChartCompatibilityMember'
          inline_birth: '#/components/schemas/InlineBirthCompatibilityMember'
    ExistingChartCompatibilityMember:
      type: object
      additionalProperties: false
      required: [sourceType, chartId]
      properties:
        sourceType:
          type: string
          const: existing_chart
        chartId: {type: string, format: uuid}
        displayLabel:
          type: [string, 'null']
          maxLength: 80
    InlineBirthCompatibilityMember:
      type: object
      additionalProperties: false
      required: [sourceType, birthData, timeBasis]
      properties:
        sourceType:
          type: string
          const: inline_birth
        birthData:
          $ref: '#/components/schemas/BirthProfileCreate'
        timeBasis:
          type: string
          enum: [civil, true_solar]
        calibrationConfirmationId:
          type: [string, 'null']
          format: uuid
        saveAsPersonalProfile:
          type: boolean
          default: false
          description: 默认只创建合盘内部依赖；true 时另存为可见个人资料，但仍不自动生成个人报告
    CompatibilityConsentAttestation:
      type: object
      additionalProperties: false
      required: [basis, confirmed, policyVersion]
      properties:
        basis:
          type: string
          enum: [both_people_present, invitation_completed, authorized_submission]
        confirmed:
          type: boolean
          const: true
        policyVersion: {type: string}
        confirmationNote:
          type: [string, 'null']
          maxLength: 300
    Report:
      type: object
      additionalProperties: false
      required:
        - id
        - kind
        - mode
        - status
        - contentLocale
        - formatLocale
        - interpretationProfileId
        - contentReleaseId
        - title
        - sections
        - versions
        - reportReferenceAt
        - createdAt
      properties:
        id: {type: string, format: uuid}
        kind:
          type: string
          enum: [personal, compatibility]
        mode:
          type: string
          enum: [template, llm_enhanced]
        status:
          type: string
          enum: [ready, processing, failed, deleting]
        contentLocale:
          $ref: '#/components/schemas/ContentLocale'
        formatLocale: {type: string}
        interpretationProfileId: {type: string}
        contentReleaseId: {type: string}
        title: {type: string}
        sections:
          type: array
          items:
            type: object
            required: [key, title, body, claimIds]
            properties:
              key: {type: string}
              title: {type: string}
              body: {type: string}
              claimIds:
                type: array
                items: {type: string, format: uuid}
        warnings:
          type: array
          items: {type: string}
        versions:
          $ref: '#/components/schemas/VersionBundle'
        reportReferenceAt: {type: string, format: date-time}
        createdAt: {type: string, format: date-time}
    ZiweiReport:
      type: object
      additionalProperties: false
      required: [id, kind, entryModule, ziweiChartId, mode, status, contentLocale, formatLocale, interpretationProfileId, contentReleaseId, title, sections, warnings, versions, reportReferenceAt, createdAt]
      properties:
        id: {type: string, format: uuid}
        kind:
          type: string
          const: ziwei
        entryModule:
          type: string
          const: ziwei_tw
        ziweiChartId: {type: string, format: uuid}
        mode:
          type: string
          enum: [template, llm_enhanced]
        status:
          type: string
          enum: [ready, processing, failed, deleting]
        contentLocale:
          type: string
          const: zh-Hant
        formatLocale:
          type: string
          const: zh-Hant-TW
        interpretationProfileId:
          type: string
          const: ziwei-tw-sanhe@1
        contentReleaseId: {type: string}
        title: {type: string}
        sections:
          type: array
          minItems: 18
          maxItems: 18
          items:
            type: object
            required: [key, title, body, claimIds]
            properties:
              key: {type: string}
              title: {type: string}
              body: {type: string}
              claimIds:
                type: array
                items: {type: string}
        warnings:
          type: array
          items: {type: string}
        versions:
          $ref: '#/components/schemas/VersionBundle'
        reportReferenceAt: {type: string, format: date-time}
        createdAt: {type: string, format: date-time}
    ZiweiCompatibilityReport:
      type: object
      additionalProperties: false
      required: [id, kind, entryModule, mode, status, contentLocale, formatLocale, interpretationProfileId, contentReleaseId, title, sections, warnings, versions, reportReferenceAt, createdAt]
      properties:
        id: {type: string, format: uuid}
        kind:
          type: string
          const: ziwei_compatibility
        entryModule:
          type: string
          const: ziwei_compatibility_tw
        ziweiCompatibilitySnapshotId: {type: string, format: uuid}
        ziweiChartAId: {type: string, format: uuid}
        ziweiChartBId: {type: string, format: uuid}
        mode:
          type: string
          enum: [template, llm_enhanced]
        status:
          type: string
          enum: [ready, processing, failed, deleting]
        contentLocale:
          type: string
          const: zh-Hant
        formatLocale:
          type: string
          const: zh-Hant-TW
        interpretationProfileId:
          type: string
          const: ziwei-tw-compat-sanhe@1
        contentReleaseId: {type: string}
        title: {type: string}
        sections:
          type: array
          minItems: 16
          maxItems: 16
          items:
            type: object
            required: [key, title, body, claimIds]
            properties:
              key: {type: string}
              title: {type: string}
              body: {type: string}
              claimIds:
                type: array
                items: {type: string}
        warnings:
          type: array
          items: {type: string}
        versions:
          $ref: '#/components/schemas/VersionBundle'
        reportReferenceAt: {type: string, format: date-time}
        createdAt: {type: string, format: date-time}
    RelationshipCreateRequest:
      type: object
      additionalProperties: false
      required: [profileAId, profileBId, relationshipType]
      properties:
        profileAId: {type: string, format: uuid}
        profileBId: {type: string, format: uuid}
        relationshipType:
          type: string
          enum: [married, partners, dating, other]
        label:
          type: [string, 'null']
          maxLength: 80
    Relationship:
      type: object
      additionalProperties: false
      required: [id, relationshipType, members, status, createdAt]
      properties:
        id: {type: string, format: uuid}
        relationshipType: {type: string}
        members:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type: object
            required: [profileId, role, consentStatus]
            properties:
              profileId: {type: string, format: uuid}
              role: {type: string, enum: [A, B]}
              consentStatus:
                type: string
                enum: [pending, granted, revoked]
        status:
          type: string
          enum: [pending_consent, active, revoked]
        createdAt: {type: string, format: date-time}
    ConsentRequest:
      type: object
      additionalProperties: false
      required: [profileId, action, policyVersion]
      properties:
        profileId: {type: string, format: uuid}
        action:
          type: string
          enum: [grant, revoke]
        policyVersion: {type: string}
    ExportCreateRequest:
      type: object
      additionalProperties: false
      required: [format, privacyMode]
      properties:
        format:
          type: string
          enum: [markdown, html, pdf]
        privacyMode:
          type: string
          enum: [full, redacted]
        expiresInHours:
          type: integer
          minimum: 1
          maximum: 720
          default: 72
    JobAccepted:
      type: object
      additionalProperties: false
      required: [jobId, status, statusUrl]
      properties:
        jobId: {type: string, format: uuid}
        status:
          type: string
          enum: [queued]
        statusUrl: {type: string}
    Job:
      type: object
      additionalProperties: false
      required: [id, type, status, createdAt, updatedAt]
      properties:
        id: {type: string, format: uuid}
        type:
          type: string
          enum: [llm_report, report_export, data_deletion, batch_recalculation]
        status:
          type: string
          enum: [queued, running, succeeded, failed, cancelled]
        progress:
          type: integer
          minimum: 0
          maximum: 100
        resultResource:
          type: [string, 'null']
        error:
          oneOf:
            - $ref: '#/components/schemas/Problem'
            - type: 'null'
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    Problem:
      type: object
      additionalProperties: false
      required: [type, title, status, code, requestId]
      properties:
        type:
          type: string
          format: uri-reference
        title: {type: string}
        status: {type: integer, minimum: 400, maximum: 599}
        code:
          type: string
          examples: [TIME_ZONE_AMBIGUOUS]
        detail: {type: [string, 'null']}
        requestId: {type: string}
        invalidParams:
          type: array
          items:
            type: object
            required: [name, reason]
            properties:
              name: {type: string}
              reason: {type: string}
              candidateValues:
                type: array
                items: {}
