openapi: 3.1.0

# ⚠️ Escrita à mão, de propósito (ver plano-docs-api.md). O guard contra drift é
# um teste que percorre `apiRouter.stack` e exige verbete aqui para toda rota
# registrada — menos /api/v1/ops/eventos, que é rota de operação e não entra.
#
# ⚠️ A superfície pública fala em CATEGORIAS DE RESTRIÇÃO, nunca no nome do
# órgão que publica. O mapa categoria → fonte interna está em plano-docs-api.md
# e NÃO deve ser reconstituído aqui, nem em exemplo, nem em comentário.

info:
  title: API monitore.ai
  version: '1.0'
  summary: Cadastro, quadro societário e restrições públicas de empresas brasileiras.
  description: |
    Consulta por CNPJ sobre bases públicas oficiais — as mesmas que alimentam
    a tela do monitore.ai.

    ## Três regras que valem para toda resposta

    **1. Erro nunca vira lista vazia.** Base fora do ar responde `503`, ou
    (no consolidado `/restricoes`) `null` na categoria e
    `indisponivel: true` no resumo. Ler ausência de dado como "nada consta" é
    o único erro inaceitável desta API.

    **2. `404` não é `200` vazio.** CNPJ inexistente é `404`. CNPJ que existe
    e está limpo é `200` com listas vazias.

    **3. CPF sai sempre mascarado**, no formato `***dddddd**` (6 dígitos do
    meio). Não há como obter o CPF completo por esta API, e o par
    nome + CPF mascarado é o identificador de pessoa física — não existe id
    de sócio na base.

    ## Autenticação

    Header `x-api-key` com a chave de 40 caracteres (`mk_live_…`). Sem
    sessão, sem OAuth: uma sessão do site **não** autoriza a API.

    Só o sha-256 da chave é guardado. Chave perdida não se reenvia — emite-se
    outra.

    ## Limites

    Sem tarifação por consulta hoje. O teto é o rate limit da borda:
    **10 req/s por chave** e **5 req/s por IP** (picos absorvidos por burst).

    ## CORS

    A API **não** manda header de CORS, de propósito: é o que impede uma
    página web de usar uma chave vazada direto do browser. O cliente é
    servidor, Excel ou agente — nunca JavaScript de página.
  contact:
    name: monitore.ai
    url: https://monitore.ai

servers:
  - url: https://api.monitore.ai
    description: Produção

security:
  - ApiKey: []

tags:
  - name: Empresas
    description: Cadastro, filiais, histórico e quadro societário.
  - name: Restrições
    description: |
      Nove categorias de restrição pública, consultáveis de uma vez ou uma a
      uma.
  - name: Sócios
    description: Pessoa física identificada por nome + CPF mascarado.

paths:
  /api/v1/empresas:
    get:
      tags: [Empresas]
      summary: Busca empresas por nome ou CNPJ
      description: Autocomplete. Menos de 2 caracteres devolve `[]` sem erro.
      operationId: buscarEmpresas
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 2 }
          example: petrobras
      responses:
        '200':
          description: Lista de sugestões (vazia quando `q` tem menos de 2 caracteres).
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/EmpresaSugestao' }
        '401': { $ref: '#/components/responses/NaoAutorizado' }
        '503':
          description: '`base-de-empresas-indisponivel`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }

  /api/v1/empresas/{cnpj}:
    get:
      tags: [Empresas]
      summary: Ficha cadastral
      description: |
        Dados cadastrais oficiais, incluindo o quadro societário (`socios`),
        com CPF mascarado.

        ⚠️ O objeto é um passthrough da base cadastral: os campos abaixo são
        os que a tela consome e estão garantidos, mas a resposta traz outros.
        Não assuma schema fechado.
      operationId: fichaEmpresa
      parameters: [{ $ref: '#/components/parameters/Cnpj' }]
      responses:
        '200':
          description: Ficha da empresa.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FichaEmpresa' }
        '400': { $ref: '#/components/responses/CnpjInvalido' }
        '401': { $ref: '#/components/responses/NaoAutorizado' }
        '404': { $ref: '#/components/responses/EmpresaNaoEncontrada' }
        '503':
          description: '`base-de-empresas-indisponivel`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }

  /api/v1/empresas/{cnpj}/filiais:
    get:
      tags: [Empresas]
      summary: Filiais da mesma raiz de CNPJ
      description: Estabelecimentos que compartilham os 8 primeiros dígitos, matriz inclusa.
      operationId: filiaisEmpresa
      parameters: [{ $ref: '#/components/parameters/Cnpj' }]
      responses:
        '200':
          description: Lista de filiais.
          content:
            application/json:
              schema:
                type: object
                properties:
                  filiais:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                      properties:
                        cnpj: { type: string }
                        nome_fantasia: { type: [string, 'null'] }
                        situacao_cadastral: { type: [string, 'null'] }
                        uf: { type: [string, 'null'] }
                        municipio: { type: [string, 'null'] }
        '400': { $ref: '#/components/responses/CnpjInvalido' }
        '401': { $ref: '#/components/responses/NaoAutorizado' }
        '503':
          description: '`base-de-empresas-indisponivel`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }

  /api/v1/empresas/{cnpj}/historico:
    get:
      tags: [Empresas]
      summary: Histórico de alterações cadastrais
      description: |
        Diferenças entre cargas mensais da base cadastral, já traduzidas para
        tópicos em português (`topicos`) — os mesmos que a tela mostra.
        Determinístico, sem IA.
      operationId: historicoEmpresa
      parameters: [{ $ref: '#/components/parameters/Cnpj' }]
      responses:
        '200':
          description: Eventos de alteração, do mais recente ao mais antigo.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  eventos:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                      properties:
                        competencia:
                          type: string
                          description: Carga em que a mudança apareceu.
                        topicos:
                          type: array
                          description: Descrição legível da mudança.
                          items: { type: string }
        '400': { $ref: '#/components/responses/CnpjInvalido' }
        '401': { $ref: '#/components/responses/NaoAutorizado' }
        '503':
          description: '`base-de-historico-indisponivel`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }

  /api/v1/empresas/{cnpj}/vinculos:
    get:
      tags: [Empresas]
      summary: Grafo societário
      description: |
        Empresas ligadas por sócio em comum, a partir da raiz do CNPJ.
        Nós e arestas prontos para desenho ou para achatar em planilha.
      operationId: vinculosEmpresa
      parameters: [{ $ref: '#/components/parameters/Cnpj' }]
      responses:
        '200':
          description: Grafo de vínculos.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  nos: { type: array, items: { type: object, additionalProperties: true } }
                  arestas: { type: array, items: { type: object, additionalProperties: true } }
        '400': { $ref: '#/components/responses/CnpjInvalido' }
        '401': { $ref: '#/components/responses/NaoAutorizado' }
        '404': { $ref: '#/components/responses/EmpresaNaoEncontrada' }
        '503':
          description: '`vinculos-nao-configurado` ou `base-de-vinculos-indisponivel`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }

  /api/v1/empresas/{cnpj}/restricoes:
    get:
      tags: [Restrições]
      summary: Consolidado das nove categorias
      description: |
        **A rota a usar.** Responde "tem pendência?" numa chamada só — pedir
        categoria a categoria custa nove requisições pelo que a tela resolve
        em uma.

        ### Degradação parcial é deliberada

        Categoria que falha volta **`null`** em `consultas` e aparece com
        **`indisponivel: true`** em `resumo.categorias` — nunca como lista
        vazia. Sem esse sinal, "0 apontamentos" seria lido como "nada consta"
        sobre uma base que estava fora do ar. Só quando **as nove** falham a
        resposta vira `503`.

        ### Régua de cor

        `vermelho` = situação em aberto hoje. `ambar` = em andamento,
        encerrado (histórico) ou homônimo indeterminado. `null` = nada
        consta. Processo em andamento **não** é condenação.

        ### `ambiental` vem paginado

        Só a primeira página entra aqui; `consultas.ambiental.resumo` já traz
        os totais reais. Para as linhas restantes, use
        `/restricoes/ambiental?pagina=`.
      operationId: restricoesEmpresa
      parameters: [{ $ref: '#/components/parameters/Cnpj' }]
      responses:
        '200':
          description: Resumo consolidado mais o retorno detalhado de cada categoria.
          content:
            application/json:
              schema:
                type: object
                required: [cnpj, resumo, consultas]
                properties:
                  cnpj: { type: string, examples: ['33000167000101'] }
                  resumo: { $ref: '#/components/schemas/ResumoRestricoes' }
                  consultas:
                    type: object
                    description: '`null` em qualquer chave significa categoria indisponível.'
                    properties:
                      financeiro: { oneOf: [{ $ref: '#/components/schemas/DetalheFinanceiro' }, { type: 'null' }] }
                      gestao: { oneOf: [{ $ref: '#/components/schemas/DetalheGestao' }, { type: 'null' }] }
                      contratacaoPublica: { oneOf: [{ $ref: '#/components/schemas/DetalheContratacaoPublica' }, { type: 'null' }] }
                      mercadoCapitais: { oneOf: [{ $ref: '#/components/schemas/DetalheMercadoCapitais' }, { type: 'null' }] }
                      ambiental: { oneOf: [{ $ref: '#/components/schemas/DetalheAmbiental' }, { type: 'null' }] }
                      trabalho: { oneOf: [{ $ref: '#/components/schemas/DetalheTrabalho' }, { type: 'null' }] }
                      tributarioFederal: { oneOf: [{ $ref: '#/components/schemas/DetalheTributarioFederal' }, { type: 'null' }] }
                      insolvencia: { oneOf: [{ $ref: '#/components/schemas/DetalheInsolvencia' }, { type: 'null' }] }
                      contasPublicas: { oneOf: [{ $ref: '#/components/schemas/DetalheContasPublicas' }, { type: 'null' }] }
        '400': { $ref: '#/components/responses/CnpjInvalido' }
        '401': { $ref: '#/components/responses/NaoAutorizado' }
        '404': { $ref: '#/components/responses/EmpresaNaoEncontrada' }
        '503':
          description: '`bases-de-restricoes-indisponiveis` (as nove falharam) ou `base-de-empresas-indisponivel`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }

  /api/v1/empresas/{cnpj}/restricoes/{categoria}:
    get:
      tags: [Restrições]
      summary: Uma categoria de restrição
      description: |
        Mesmo conteúdo que a chave correspondente de `consultas` no
        consolidado, quando o cliente só quer uma categoria. O shape da
        resposta **varia por categoria** — veja a tabela abaixo.

        | Categoria | Sujeitos | Natureza | O que traz |
        |---|---|---|---|
        | `financeiro` | CNPJ + CPF | `multa` | Processo sancionador com penalidade e multa por instância, recurso e a multa que vale hoje |
        | `gestao` | CNPJ + CPF | `sancao` | Penalidade, prazo em anos, início e fim do cumprimento, e se o prazo ainda corre |
        | `contratacaoPublica` | CNPJ + CPF | `sancao` | Categoria da sanção, vigência de início e fim, valor quando houver, e acordos de leniência |
        | `mercadoCapitais` | CNPJ + CPF | `processo` | Processo, objeto e ementa, situação, fase atual, última movimentação e contagem de acusações contra condenações |
        | `ambiental` | CNPJ + CPF | `multa` | Autuações e embargos com valor, área em hectares, local, status e descrição (**paginado**) |
        | `trabalho` | CNPJ + CPF | `sancao` | Ano da ação fiscal, trabalhadores envolvidos, local, decisão e se está na lista atual ou já esteve |
        | `tributarioFederal` | CNPJ + CPF | `divida` | Valor consolidado, inscrições em cobrança e ajuizadas, quebra por origem e o detalhe das inscrições |
        | `insolvencia` | **só CNPJ** | `estado` | Estado, nome como grafado na fonte, e as publicações que o sustentam |
        | `contasPublicas` | CNPJ + CPF | `sancao` | Processo e acórdão, trânsito em julgado, links oficiais e — na inabilitação — prazo final |
      operationId: restricaoPorCategoria
      parameters:
        - { $ref: '#/components/parameters/Cnpj' }
        - name: categoria
          in: path
          required: true
          description: |
            **Mesma grafia** que `resumo.categorias[].categoria` e que as
            chaves de `consultas` — camelCase, não kebab-case. É deliberado:
            o cliente lê a categoria do consolidado e monta esta URL com ela
            direto, sem tabela de conversão no meio.
          schema:
            type: string
            enum:
              - financeiro
              - gestao
              - contratacaoPublica
              - mercadoCapitais
              - ambiental
              - trabalho
              - tributarioFederal
              - insolvencia
              - contasPublicas
        - name: pagina
          in: query
          description: Só em `ambiental`. Ignorado nas demais categorias.
          schema: { type: integer, minimum: 1, maximum: 10000, default: 1 }
      responses:
        '200':
          description: Detalhe da categoria pedida.
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: '#/components/schemas/DetalheFinanceiro' }
                  - { $ref: '#/components/schemas/DetalheGestao' }
                  - { $ref: '#/components/schemas/DetalheContratacaoPublica' }
                  - { $ref: '#/components/schemas/DetalheMercadoCapitais' }
                  - { $ref: '#/components/schemas/DetalheAmbiental' }
                  - { $ref: '#/components/schemas/DetalheTrabalho' }
                  - { $ref: '#/components/schemas/DetalheTributarioFederal' }
                  - { $ref: '#/components/schemas/DetalheInsolvencia' }
                  - { $ref: '#/components/schemas/DetalheContasPublicas' }
        '400':
          description: '`cnpj-invalido`, `categoria-invalida` ou `pagina-invalida`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }
        '401': { $ref: '#/components/responses/NaoAutorizado' }
        '404': { $ref: '#/components/responses/EmpresaNaoEncontrada' }
        '503':
          description: |
            `base-da-categoria-indisponivel` — a base daquela categoria não
            respondeu. Um só código para as nove: o código de erro também
            nomeava o órgão (`base-de-sancoes-bacen-indisponivel`) e era
            vazamento pela porta dos fundos.

            ⚠️ Em `ambiental`, `base-sem-carga` significa **base ainda não
            carregada**, nunca "zero apontamentos".
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }

  /api/v1/socios:
    get:
      tags: [Sócios]
      summary: Busca sócios por nome
      operationId: buscarSocios
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
          example: joao da silva
      responses:
        '200':
          description: Pessoas encontradas, com CPF mascarado.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  additionalProperties: true
                  properties:
                    nome: { type: string }
                    cpf:
                      type: string
                      description: Sempre mascarado.
                      examples: ['***123456**']
        '401': { $ref: '#/components/responses/NaoAutorizado' }
        '503':
          description: '`base-de-empresas-indisponivel`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }

  /api/v1/socios/empresas:
    get:
      tags: [Sócios]
      summary: Empresas de que a pessoa participa
      description: |
        Não existe id de sócio na base: a pessoa é o par **nome + CPF
        mascarado**. É por isso que os dois vêm em querystring, e não como
        `/socios/{id}/empresas`.

        Ambos são obrigatórios. Só o nome é ambíguo demais para responder.
      operationId: empresasDoSocio
      parameters:
        - name: nome
          in: query
          required: true
          schema: { type: string, maxLength: 500 }
        - name: cpf
          in: query
          required: true
          description: CPF mascarado, exatamente no formato devolvido pelas outras rotas.
          schema: { type: string, pattern: '^\*{3}\d{6}\*{2}$' }
          example: '***123456**'
      responses:
        '200':
          description: Empresas ligadas à pessoa.
          content:
            application/json:
              schema:
                type: object
                properties:
                  empresas:
                    type: array
                    items: { type: object, additionalProperties: true }
        '400':
          description: '`socio-invalido` — nome vazio ou CPF fora do formato mascarado.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }
        '401': { $ref: '#/components/responses/NaoAutorizado' }
        '503':
          description: '`vinculos-nao-configurado` ou `base-de-vinculos-indisponivel`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Erro' }

components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        Chave de 40 caracteres começando em `mk_live_`. Só em header — nunca
        em querystring, que fica gravada em log de proxy.

  parameters:
    Cnpj:
      name: cnpj
      in: path
      required: true
      description: 14 dígitos. Pontuação é aceita e descartada.
      schema: { type: string }
      example: '33000167000101'

  responses:
    NaoAutorizado:
      description: |
        `api-key-ausente` (401), `api-key-invalida` (401) ou
        `api-key-revogada` (403). Códigos distintos de propósito: cada um pede
        uma ação diferente.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Erro' }
    CnpjInvalido:
      description: '`cnpj-invalido` — não são 14 dígitos.'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Erro' }
    EmpresaNaoEncontrada:
      description: |
        `empresa-nao-encontrada`. Distinto de `200` com listas vazias, que
        significa "empresa existe e nada consta".
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Erro' }

  schemas:
    Erro:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Código estável, legível por máquina. Nunca traga a mensagem para o usuário final sem tradução.
          examples: [cnpj-invalido]

    EmpresaSugestao:
      type: object
      additionalProperties: true
      properties:
        cnpj: { type: string }
        razao_social: { type: string }
        nome_fantasia: { type: [string, 'null'] }
        uf: { type: [string, 'null'] }
        municipio: { type: [string, 'null'] }

    FichaEmpresa:
      type: object
      description: Passthrough da base cadastral — traz mais campos que os listados.
      additionalProperties: true
      properties:
        cnpj: { type: string }
        razao_social: { type: string }
        nome_fantasia: { type: [string, 'null'] }
        situacao_cadastral: { type: [string, 'null'] }
        data_inicio_atividade: { type: [string, 'null'] }
        cnae_fiscal: { type: [string, 'null'] }
        capital_social: { type: [string, number, 'null'] }
        socios:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              nome_socio: { type: string }
              cnpj_cpf_socio:
                type: string
                description: CPF sempre mascarado; CNPJ de sócio PJ vem com 14 dígitos.
                examples: ['***123456**']
              qualificacao_socio: { type: [string, 'null'] }

    ResumoRestricoes:
      type: object
      description: Veredito cruzado das nove categorias — o mesmo que a tela pinta.
      required: [severidade, parcial, total, categorias]
      properties:
        severidade:
          type: [string, 'null']
          enum: [vermelho, ambar, null]
          description: Pior severidade entre as categorias. `null` = nada consta.
        parcial:
          type: boolean
          description: '`true` quando alguma categoria veio indisponível — a resposta **não** é conclusiva.'
        total:
          type: integer
          description: Total de apontamentos somados.
        categorias:
          type: array
          items: { $ref: '#/components/schemas/CategoriaResumida' }

    CategoriaResumida:
      type: object
      required: [categoria, sujeitos, natureza, n, nEmpresa, nSocios, severidade, historico, ambiguo, indisponivel]
      properties:
        categoria:
          type: string
          enum: [financeiro, gestao, contratacaoPublica, mercadoCapitais, ambiental, trabalho, tributarioFederal, insolvencia, contasPublicas]
        sujeitos:
          type: array
          description: |
            Quem esta categoria pode acusar. `empresa` = o CNPJ consultado;
            `socio` = pessoa do quadro societário, identificada por nome +
            CPF mascarado.

            `insolvencia` é a única categoria `["empresa"]` — pessoa física
            não entra em recuperação judicial.
          items: { type: string, enum: [empresa, socio] }
          examples: [['empresa', 'socio']]
        natureza:
          type: string
          description: |
            O que o registro é. **`multa` e `sancao` não são opostos**: multa
            é a sanção que tem valor em dinheiro, `sancao` é a punição sem
            valor (impedimento, inidoneidade, inabilitação).

            | Valor | Significa |
            |---|---|
            | `multa` | Sanção com valor em reais |
            | `sancao` | Punição sem valor: impedimento, inidoneidade, inabilitação |
            | `divida` | Débito — **não é punição** |
            | `processo` | Apuração em curso ou já julgada — **não é condenação** |
            | `estado` | Situação jurídica da empresa |

            Não some `divida` com `sancao` num total só: pinta de inadimplente
            quem foi punido, e de punido quem só deve.
          enum: [multa, sancao, divida, processo, estado]
        n:
          type: integer
          description: Total de apontamentos. Sempre igual a `nEmpresa + nSocios`.
        nEmpresa:
          type: integer
          description: Apontamentos atribuídos ao CNPJ consultado.
        nSocios:
          type: integer
          description: |
            Apontamentos atribuídos a pessoas do quadro societário. Em
            `insolvencia` é sempre `0` por construção, nunca por ausência de
            achado.
        severidade:
          type: [string, 'null']
          enum: [vermelho, ambar, null]
        historico:
          type: boolean
          description: Achado já encerrado — âmbar, e a tela escreve "(histórico)".
        ambiguo:
          type: boolean
          description: Homônimo indeterminado. Âmbar sem contagem — não é "nada consta".
        indisponivel:
          type: boolean
          description: A consulta a esta categoria falhou. `n = 0` aqui **não** significa ausência de apontamento.

    CargaAtualizadaEm:
      type: [string, 'null']
      format: date-time
      description: Última carga bem-sucedida da base. `null` = carga não informada.

    DetalheFinanceiro:
      type: object
      description: |
        Processos sancionadores com penalidade aplicada. O registro é **por
        instância**: a mesma decisão pode ter sido reformada em recurso, então
        os campos vêm duplicados para 1ª e 2ª instância.
      properties:
        cnpj: { type: string }
        sancoes:
          type: array
          items:
            type: object
            properties:
              pas: { type: string, description: Número do processo administrativo sancionador. }
              nome: { type: string }
              cpfCnpj:
                type: string
                description: CPF mascarado ou CNPJ de quem foi apontado.
              dataCitacao: { type: [string, 'null'], format: date }
              numeroDecisao1Instancia: { type: [string, 'null'] }
              dataDecisao1Instancia: { type: [string, 'null'], format: date }
              tipoPenalidade1Instancia:
                type: [string, 'null']
                description: Multa, advertência, inabilitação, suspensão.
              duracaoPena1Instancia:
                type: [number, 'null']
                description: Em anos, quando a penalidade tem prazo.
              valorMulta1Instancia: { type: [number, 'null'] }
              apresentouRecurso: { type: [string, 'null'] }
              numeroDecisao2Instancia: { type: [string, 'null'] }
              dataDecisao2Instancia: { type: [string, 'null'], format: date }
              tipoPenalidade2Instancia: { type: [string, 'null'] }
              duracaoPena2Instancia: { type: [number, 'null'] }
              valorMulta2Instancia: { type: [number, 'null'] }
              situacao: { type: [string, 'null'] }
              estado:
                type: object
                description: |
                  Classificação já resolvida das duas instâncias — use esta em
                  vez de reimplementar a régua. São **três perguntas
                  independentes**, não um rótulo só.

                  ⚠️ Campo **derivado da data de hoje**: muda sozinho quando um
                  prazo de recurso vence, mesmo sem a fonte publicar nada.
                required: [vigencia, natureza, definitividade]
                properties:
                  vigencia:
                    type: string
                    description: |
                      Se ainda há apontamento em aberto.

                      ⚠️ Situação nova ou em branco na fonte classifica como
                      `PENDENCIA_ATUAL`, **nunca** como "sem apontamento":
                      esconder sanção de quem consulta é a falha inaceitável.
                  natureza:
                    type: string
                    description: Tipo da penalidade. `NAO_CLASSIFICADA` quando a fonte traz situação que a régua ainda não conhece.
                  definitividade:
                    type: string
                    description: |
                      Se a decisão ainda comporta recurso administrativo.
                      `INDETERMINADO` existe e **não** é sinônimo de
                      definitivo — não trate os dois juntos.
              valorMultaVigente:
                type: [number, 'null']
                description: |
                  A multa que vale hoje: a de 2ª instância quando houve
                  recurso julgado, a de 1ª caso contrário. Não some as duas.
        cargaAtualizadaEm: { $ref: '#/components/schemas/CargaAtualizadaEm' }

    DetalheGestao:
      type: object
      description: Impedimentos de exercer cargo de gestão, com prazo determinado.
      properties:
        cnpj: { type: string }
        resultados:
          type: array
          items:
            type: object
            properties:
              tipo:
                type: string
                description: Qual das duas listas de impedimento originou o registro.
              socioNome: { type: string }
              socioCpfMascarado: { type: string, examples: ['***123456**'] }
              nomeNaFonte:
                type: string
                description: Nome como publicado na fonte, para conferir a identificação.
              pas: { type: string, description: Número do processo sancionador. }
              penalidade: { type: string }
              prazoEmAnos: { type: [number, 'null'] }
              inicioCumprimento: { type: [string, 'null'], format: date }
              prazoFinalPenalidade: { type: [string, 'null'], format: date }
              emCurso:
                type: boolean
                description: |
                  Prazo ainda correndo hoje — é o que separa impedimento ativo
                  de já cumprido.

                  ⚠️ Derivado da data de hoje.
        totalSocios: { type: integer }
        cargaAtualizadaEm: { $ref: '#/components/schemas/CargaAtualizadaEm' }

    DetalheContratacaoPublica:
      type: object
      description: |
        Impedimentos de contratar com a administração pública e acordos de
        leniência. `empresa` e `socios` vêm separados porque a decisão de
        crédito muda conforme o apontamento seja do CNPJ ou de alguém do
        quadro.
      properties:
        empresa:
          type: array
          items: { $ref: '#/components/schemas/RegistroContratacaoPublica' }
        socios:
          type: array
          items:
            type: object
            properties:
              nome: { type: string }
              cpfMascarado: { type: string, examples: ['***123456**'] }
              sancoes:
                type: array
                items: { $ref: '#/components/schemas/RegistroContratacaoPublica' }
        cargaAtualizadaEm: { $ref: '#/components/schemas/CargaAtualizadaEm' }

    RegistroContratacaoPublica:
      type: object
      properties:
        tipo:
          type: string
          enum: [inidoneidade, anticorrupcao, leniencia]
          description: |
            `inidoneidade` — impedimento ou suspensão de licitar e contratar.
            `anticorrupcao` — punição por ato lesivo à administração.
            `leniencia` — acordo de colaboração firmado.
        id_registro: { type: string }
        nome: { type: string }
        categoria:
          type: [string, 'null']
          description: Modalidade da sanção conforme publicada na fonte.
        data_inicio: { type: [string, 'null'], format: date }
        data_final: { type: [string, 'null'], format: date }
        valor_multa:
          type: [string, 'null']
          description: |
            Só `anticorrupcao` publica valor; nos demais vem `null`.

            ⚠️ String, não número — é `numeric` do banco. Some em centavos.
        ativa_na_fonte:
          type: [boolean, 'null']
          description: |
            Vigência respondida pela própria fonte. Preferir a este campo em
            vez de calcular por `data_final`: leniência não usa data para
            dizer se está cumprida.
        encerrada:
          type: boolean
          description: '⚠️ Derivado da data de hoje.'

    DetalheMercadoCapitais:
      type: object
      description: |
        Processos sancionadores. ⚠️ **Processo não é condenação** — o `resumo`
        separa os dois, e é por isso que esta categoria tem natureza
        `processo`.
      properties:
        resumo:
          type: object
          properties:
            acusacoes: { type: integer, description: Total de processos em que o alvo é acusado. }
            condenacoes: { type: integer, description: Subconjunto já julgado com condenação. }
            emAndamento: { type: integer, description: Ainda sem julgamento. }
        processos:
          type: array
          items:
            type: object
            properties:
              nup: { type: string, description: Número único do processo. }
              nomeAcusado: { type: string }
              situacao: { type: [string, 'null'] }
              dataSituacao: { type: [string, 'null'], format: date }
              cnpj:
                type: [string, 'null']
                description: '`null` quando o acusado é pessoa física — o CPF não sai nem mascarado aqui.'
              objeto: { type: [string, 'null'] }
              ementa: { type: [string, 'null'] }
              dataAbertura: { type: [string, 'null'], format: date }
              faseAtual: { type: [string, 'null'] }
              subfaseAtual: { type: [string, 'null'] }
              dataUltimaMovimentacao: { type: [string, 'null'], format: date }
              observacao: { type: [string, 'null'] }
              matchPor:
                type: string
                enum: [cnpj, nome]
                description: Como o processo foi ligado ao alvo. `nome` é mais fraco que `cnpj`.
              fonteMatch: { type: [string, 'null'] }
              scoreMatch: { type: [number, 'null'] }
              revisadoHumano: { type: boolean }
              documento:
                type: object
                description: |
                  Confiança da ligação entre o processo e o alvo — a fonte nem
                  sempre publica o documento do acusado.

                  Trate `sugerido` como indício, não como fato: é inferência de
                  IA que ninguém conferiu ainda.
                properties:
                  status:
                    type: string
                    enum: [verificado, revisado, sugerido, identificado, sem_documento]
                  label:
                    type: string
                    description: A mesma classificação em texto, pronta para exibir.
                    examples: ['Sugerido por IA — não revisado']

    DetalheAmbiental:
      type: object
      description: |
        Autuações e embargos. Única categoria paginada — `resumo` traz os
        totais reais, independentes da página pedida.
      properties:
        resumo:
          type: object
          properties:
            total_apontamentos: { type: [integer, string] }
            total_vigentes: { type: [integer, string] }
        paginacao:
          type: object
          description: Percorra até `totalPaginas`; o `resumo` já é o total, não some as páginas.
          properties:
            pagina: { type: integer }
            porPagina: { type: integer }
            total: { type: integer }
            totalPaginas: { type: integer }
        documentosConsultados:
          type: integer
          description: Quantos alvos (empresa + sócios) foram resolvidos para esta consulta.
        apontamentos:
          type: array
          items:
            type: object
            properties:
              tipo:
                type: string
                enum: [autuacao, embargo]
                description: |
                  `autuacao` = auto de infração com multa. `embargo` =
                  interdição de área ou atividade, que não tem valor.
              id: { type: string }
              cpf_cnpj: { type: string }
              nome: { type: [string, 'null'] }
              numero_processo: { type: [string, 'null'] }
              data_evento: { type: [string, 'null'], format: date }
              uf: { type: [string, 'null'] }
              municipio: { type: [string, 'null'] }
              status: { type: [string, 'null'] }
              descricao: { type: [string, 'null'], description: Enquadramento da infração. }
              valor_multa:
                type: [string, 'null']
                description: '⚠️ String (`numeric` do banco). Some em centavos.'
              area_hectares: { type: [string, 'null'] }

    DetalheTrabalho:
      type: object
      description: |
        Inclusão em cadastro de empregadores autuados por trabalho análogo ao
        de escravo. Distingue **estar na lista hoje** de **já ter estado** —
        são situações diferentes e não podem colapsar numa só.
      properties:
        empresa: { $ref: '#/components/schemas/ResultadoTrabalho' }
        socios:
          type: array
          items:
            allOf:
              - { $ref: '#/components/schemas/ResultadoTrabalho' }
              - type: object
                properties:
                  nome: { type: string }
                  cpfMascarado: { type: string, examples: ['***123456**'] }
        totalSocios: { type: integer }
        consultadoEm: { type: string, format: date-time }
        ultimaImportacao: { $ref: '#/components/schemas/CargaAtualizadaEm' }

    ResultadoTrabalho:
      type: object
      properties:
        status: { type: string }
        naListaAtual: { type: boolean }
        jaEsteve:
          type: boolean
          description: 'Constou em edição anterior do cadastro. `true` com `naListaAtual: false` é histórico, não situação corrente.'
        ocorrencias:
          type: array
          items:
            type: object
            properties:
              empregador: { type: string }
              tipoDocumento: { type: string, enum: [CNPJ, CPF] }
              documento: { type: string }
              anoAcaoFiscal: { type: [integer, 'null'] }
              uf: { type: [string, 'null'] }
              estabelecimento: { type: [string, 'null'] }
              trabalhadoresEnvolvidos: { type: [integer, 'null'] }
              cnae: { type: [string, 'null'] }
              decisaoAdministrativa: { type: [string, 'null'] }
              ultimaInclusaoCadastro: { type: [string, 'null'], format: date }
              fundamentoInclusao:
                type: string
                enum: [administrativa, judicial]

    DetalheTributarioFederal:
      type: object
      description: |
        Dívida ativa federal, agregada por alvo. **Não é sanção** — é débito
        não pago.

        ⚠️ Todo valor é `numeric` do banco, entregue como **string** para não
        perder precisão. Somar em ponto flutuante corrompe centavos.
      properties:
        empresa:
          oneOf:
            - { $ref: '#/components/schemas/ResultadoTributarioFederal' }
            - type: 'null'
        socios:
          type: array
          items: { $ref: '#/components/schemas/ResultadoTributarioFederal' }

    ResultadoTributarioFederal:
      type: object
      properties:
        nome: { type: string }
        cpfMascarado: { type: string }
        nomeDevedor:
          type: [string, 'null']
          description: Nome do devedor como publicado na fonte, já confirmado contra o alvo.
        quantidade: { type: integer, description: Total real de inscrições. }
        valorTotal: { type: string, examples: ['1234567.89'] }
        emCobranca: { type: integer }
        ajuizadas: { type: integer, description: Inscrições já levadas a execução fiscal. }
        categorias:
          type: array
          description: Quebra por origem do débito — previdenciário, tributário, não tributário.
          items:
            type: object
            properties:
              origem: { type: string }
              quantidade: { type: integer }
              valorTotal: { type: string }
        inscricoes:
          type: array
          description: 'Detalhe, no **máximo 100 linhas** (ver `truncado`).'
          items:
            type: object
            properties:
              numero_inscricao: { type: string }
              tipo_devedor: { type: [string, 'null'] }
              uf_devedor: { type: [string, 'null'] }
              data_inscricao: { type: [string, 'null'], format: date }
              valor_consolidado: { type: [string, 'null'] }
              unidade_responsavel: { type: [string, 'null'] }
              indicador_ajuizado: { type: [string, 'null'] }
              receita_principal: { type: [string, 'null'] }
              situacao_inscricao: { type: [string, 'null'] }
        truncado:
          type: boolean
          description: '`inscricoes` foi cortado em 100. `quantidade` e `valorTotal` continuam sendo o total real.'
        descartados:
          type: integer
          description: Devedores que dividem os mesmos 6 dígitos publicados e **não** foram atribuídos a este alvo.
        ambiguo:
          type: boolean
          description: |
            A fonte publica só 6 dígitos do CPF. `true` = dois devedores
            igualmente compatíveis, indeterminação real.

            ⚠️ `false` com `descartados > 0` é resposta **negativa** — o
            homônimo foi descartado por incompatibilidade de nome. Não é
            inconclusivo.

    DetalheInsolvencia:
      type: object
      description: |
        Recuperação judicial e falência. Só para a empresa — pessoa física não
        entra em recuperação judicial.
      properties:
        estado:
          type: [string, 'null']
          description: |
            Derivado do **aditamento obrigatório ao nome empresarial**
            (art. 69 da Lei 11.101/2005), não do texto da publicação: o texto
            cita doutrina e hipótese, e classificar por ele já pintou empresa
            saudável de falida.

            `null` = nada afirmável, mesmo havendo publicações.
        nomeNaFonte:
          type: [string, 'null']
          description: Nome como a fonte o grafa, com o aditamento preservado — é a evidência do `estado`.
        publicacoes: { type: integer, description: Publicações que citam a empresa como sujeito. }
        indeterminadas:
          type: integer
          description: |
            Citações sem sinal de sujeito — a fonte lista **todas as partes**
            do processo, então a empresa pode aparecer como credora ou
            terceira. Não são positivos, e não são "nada consta".
        eventos:
          type: array
          items:
            type: object
            properties:
              publicacaoId: { type: string }
              data: { type: string, format: date }
              numeroProcesso: { type: [string, 'null'] }
              siglaTribunal: { type: [string, 'null'] }
              link: { type: [string, 'null'] }
              foraDaJanela:
                type: boolean
                description: Evento terminal recuperado fora da janela móvel de 45 dias.
        truncado: { type: boolean }
        ultimaImportacao: { $ref: '#/components/schemas/CargaAtualizadaEm' }

    DetalheContasPublicas:
      type: object
      description: |
        Reúne duas listas de natureza diferente, e a diferença muda a leitura:
        **inabilitação** tem prazo e pode estar vigente; **contas julgadas
        irregulares** é decisão transitada em julgado, sem prazo, sem valor e
        sem status de quitação na fonte — nem "aberto" nem "encerrado" é
        afirmável.
      properties:
        cnpj: { type: string }
        empresa:
          type: array
          items: { $ref: '#/components/schemas/RegistroContasPublicas' }
        socios:
          type: array
          items:
            type: object
            properties:
              nome: { type: string }
              cpfMascarado: { type: string, examples: ['***123456**'] }
              registros:
                type: array
                items: { $ref: '#/components/schemas/RegistroContasPublicas' }
        totalSocios: { type: integer }
        ultimaImportacao: { $ref: '#/components/schemas/CargaAtualizadaEm' }

    RegistroContasPublicas:
      type: object
      properties:
        fonte:
          type: string
          enum: [inabilitacao, contas-irregulares]
          description: Qual das duas listas originou o registro — muda tudo que vem abaixo.
        id_registro: { type: string }
        nome: { type: string }
        uf: { type: [string, 'null'] }
        municipio: { type: [string, 'null'] }
        numeroProcesso: { type: string }
        numeroAcordao: { type: [string, 'null'] }
        linkDeliberacoes: { type: [string, 'null'], format: uri }
        linkProcesso: { type: [string, 'null'], format: uri }
        dataTransitoJulgado: { type: [string, 'null'], format: date }
        dataFinal:
          type: [string, 'null']
          description: Fim do prazo. Só em `inabilitacao` — contas irregulares não têm prazo.
        vigente:
          type: [boolean, 'null']
          description: |
            `true`/`false` só em `inabilitacao`. **`null` em
            `contas-irregulares`**, onde "vigente" não é pergunta que a fonte
            responda — e onde `null` não significa "não vigente".

            ⚠️ Derivado da data de hoje.
