{
  "openapi": "3.0.3",
  "info": {
    "title": "Lens API",
    "version": "1.8.0",
    "description": "O Lens organiza a investigação de um sinistro de seguro auto em três etapas: **Coleta** (o caso, os envolvidos e os documentos), **Análise** (as nove dimensões que cruzam o que foi coletado) e **Investigação** (a diligência que responde aos alertas e o desfecho do caso).\n\nTudo gira em torno de um **caso** (`research`): ele tem **envolvidos** (`entity` — pessoa, empresa ou veículo) e **documentos** (`document` — o BO, a apólice, a CNH, o CRLV, as fotos do veículo). O caso sempre ocupa uma **raia** do quadro kanban do workspace, e o Lens o move sozinho a cada acontecimento.\n\nAs rotas longas são **assíncronas**: o envio dos documentos e o pedido de análise respondem `202` e um worker trabalha depois. O andamento das duas sempre sai no `GET` do caso, em `documents` e `analysis`.\n\nTodas as rotas ficam em `https://services.zarv.com` e usam token Bearer JWT. O workspace do token precisa ser o do caminho.",
    "contact": {
      "name": "Zarv",
      "url": "https://developers.zarv.com"
    }
  },
  "servers": [
    {
      "url": "https://services.zarv.com",
      "description": "Produção"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Coleta",
      "description": "Abrir o caso, declarar os envolvidos e enviar os documentos. É a etapa em que o caso ganha matéria: o envio em lote lê cada arquivo com um agente de IA e monta o caso a partir das leituras."
    },
    {
      "name": "Análise",
      "description": "Rodar as nove dimensões sobre o caso coletado e acompanhar o resultado. O pedido responde 202 e um worker roda a análise; o veredito de cada dimensão sai no GET do caso."
    },
    {
      "name": "Investigação",
      "description": "A etapa depois da análise: anexar a evidência que responde a um alerta, incluir o envolvido que a apuração revelou, pedir uma nova análise e mover o caso para o desfecho (Aprovado ou Reprovado)."
    },
    {
      "name": "Quadro",
      "description": "As raias do quadro kanban do workspace, nas três fases (COLETA, ANALISE e DESFECHO). O quadro nasce pronto, com nove raias, na primeira leitura."
    },
    {
      "name": "Operação",
      "description": "Saúde do serviço."
    }
  ],
  "paths": {
    "/api/v1/lens/{workspaceId}/research": {
      "get": {
        "operationId": "listResearches",
        "summary": "Listar casos do workspace",
        "description": "Lista os casos do workspace, do mais recentemente editado para o mais antigo, com `total` do filtro inteiro e a página em `data`.\n\nTodos os filtros são opcionais e se combinam. `kanbanId` é a página de uma raia do quadro — é com ele que o front monta cada coluna. A raia **Abertura** traz também os casos antigos, que nasceram antes de o caso guardar a raia.\n\n`search` procura pelo `value` de um envolvido: uma placa (que sempre tem letra) ou um CPF/CNPJ (que só tem dígito). Uma busca nunca acha a outra.\n\nCada item traz as contagens de filhos (`vehicleCount`, `entityCount`, `documentCount`), que saem da mesma fonte do detalhe — a lista e o card nunca divergem.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "name": "userId",
            "in": "query",
            "description": "Filtra pelos casos abertos por este usuário.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "domain",
            "in": "query",
            "description": "Filtra pelo domínio do caso (por exemplo, `auto`).",
            "required": false,
            "schema": {
              "type": "string",
              "example": "auto"
            }
          },
          {
            "name": "objective",
            "in": "query",
            "description": "Filtra pelo objetivo do caso.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "sinistro"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filtra pelo status do caso.",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ResearchStatus"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Placa ou CPF/CNPJ de um envolvido. Devolve os casos em que esse envolvido aparece.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "TST1A23"
            }
          },
          {
            "name": "kanbanId",
            "in": "query",
            "description": "Devolve só os casos nesta raia do quadro. O `total` passa a ser o da raia.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Tamanho da página. Ausente, zero ou negativo vale 10.",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10,
              "example": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Quantos itens pular. Negativo vale zero; além do fim devolve uma página vazia, com o `total` intacto.",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "example": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A página de casos e o total do filtro.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchPage"
                },
                "example": {
                  "data": [
                    {
                      "id": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f",
                      "workspaceId": "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607",
                      "userId": "9c8b7a65-4321-4def-9876-0123456789ab",
                      "domain": "auto",
                      "objective": "sinistro",
                      "reportAddress": "Av. Paulista, 1000 - São Paulo/SP",
                      "reportTime": "2026-11-03 23:48",
                      "reportText": "Colisão traseira no cruzamento, sem vítimas.",
                      "doubts": ["O condutor estava autorizado a dirigir?"],
                      "status": "CREATED",
                      "incidentType": "colisao",
                      "vehicleCount": 1,
                      "entityCount": 2,
                      "documentCount": 7,
                      "kanbanId": "0f3b5d21-9c47-5e8a-b6d2-1a4c7e9f03b8",
                      "createdAt": "2026-11-04T12:00:00Z",
                      "updatedAt": "2026-11-04T12:31:08Z"
                    }
                  ],
                  "total": 42
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          }
        }
      },
      "post": {
        "operationId": "createResearch",
        "summary": "Abrir um caso",
        "description": "Abre um caso no workspace e o põe na raia **Abertura** do quadro.\n\nO **autor** do caso é o usuário do token, nunca o `userId` do corpo — esse campo é ignorado. Abrir um caso sem usuário autenticado é recusado com `401` antes de qualquer gravação.\n\n`incidentType` decide quais análises rodam depois: a dimensão de posse só roda em roubo e furto, a de croqui só em colisão. Deixá-lo vazio continua valendo — o fluxo o deduz do relato —, mas um tipo declarado é sempre melhor que um deduzido.\n\n`reportTime` é o instante do fato. Mande-o com o fuso do lugar do fato: um furto declarado às 23:48 em Ribeirão Pires é 02:48Z do dia seguinte, e quem formatar sem saber onde o fato aconteceu imprime o dia errado.\n\n### Idempotência\n\nMande o header `Idempotency-Key` com um valor único por caso (um UUID, por exemplo). Repetir a requisição com a mesma chave e o mesmo corpo devolve o caso que já existe, com `200` e o header `Idempotent-Replayed: true`, sem abrir um segundo. A mesma chave com um corpo diferente responde `409 IDEMPOTENCY_KEY_REUSED`.\n\nSem a chave, duas chamadas abrem dois casos — nunca se deduplica por conteúdo.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateResearchRequest"
              },
              "example": {
                "domain": "auto",
                "objective": "sinistro",
                "status": "CREATED",
                "incidentType": "colisao",
                "reportAddress": "Av. Paulista, 1000 - São Paulo/SP",
                "reportTime": "2026-11-03T23:48:00-03:00",
                "reportText": "Colisão traseira no cruzamento, sem vítimas.",
                "doubts": ["O condutor estava autorizado a dirigir?"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replay da mesma `Idempotency-Key` com o mesmo corpo: o caso que já existia, sem abrir outro.",
            "headers": {
              "Idempotent-Replayed": {
                "description": "Sempre `true` nesta resposta.",
                "schema": {
                  "type": "string",
                  "example": "true"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchCreated"
                }
              }
            }
          },
          "201": {
            "description": "O caso foi aberto, na raia Abertura.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchCreated"
                },
                "example": {
                  "id": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f",
                  "workspaceId": "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607",
                  "userId": "9c8b7a65-4321-4def-9876-0123456789ab",
                  "domain": "auto",
                  "objective": "sinistro",
                  "reportAddress": "Av. Paulista, 1000 - São Paulo/SP",
                  "reportTime": "2026-11-03T23:48:00-03:00",
                  "reportText": "Colisão traseira no cruzamento, sem vítimas.",
                  "doubts": ["O condutor estava autorizado a dirigir?"],
                  "status": "CREATED",
                  "incidentType": "colisao",
                  "kanbanId": "0f3b5d21-9c47-5e8a-b6d2-1a4c7e9f03b8",
                  "createdAt": "2026-11-04T12:00:00Z",
                  "updatedAt": "2026-11-04T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Corpo inválido: falta `domain`, `objective` ou `status`, o status não é do vocabulário, ou `incidentType` é um valor inventado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "objective is required"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido, ou o gateway não propagou o usuário (`authenticated user is required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "authenticated user is required"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "409": {
            "description": "A mesma `Idempotency-Key` já abriu um caso com outro corpo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "idempotency key already used with a different payload",
                  "code": "IDEMPOTENCY_KEY_REUSED"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/lens/{workspaceId}/{userId}/research": {
      "get": {
        "operationId": "listUserResearches",
        "summary": "Listar casos de um usuário",
        "description": "Lista os casos abertos por um usuário dentro do workspace, com o mesmo envelope `{ data, total }` da listagem do workspace. O `userId` do caminho é filtro dentro do workspace autorizado, não identidade: quem autentica continua sendo o token.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "O autor dos casos.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Tamanho da página. Ausente, zero ou negativo vale 10.",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Quantos itens pular.",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A página de casos do usuário e o total.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserResearchPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          }
        }
      }
    },
    "/api/v1/lens/{workspaceId}/research/{id}": {
      "get": {
        "operationId": "getResearch",
        "summary": "Consultar um caso",
        "description": "O caso inteiro: o relato, as contagens de filhos, a raia do quadro e — o que mais importa para quem integra — o andamento das duas rotas assíncronas.\n\n- **`documents`** é o envio dos documentos: o status, o progresso de 0 a 100, o resultado de cada arquivo (`saved`, `read`, `read_failed` ou `left_out`, com o motivo) e a **montagem do caso** (`documents.assembly`), em que a reconciliação transforma as leituras em envolvidos e preenche o relato.\n- **`analysis`** é a análise: o status, o código do erro quando falhou e o `result`, com um campo por dimensão (`dimdocumental`, `dimchronology`, …).\n\nCaso sem envio tem `documents` com a mesma forma, vazia; caso sem análise tem `analysis.status` vazio e `result` nulo. Nenhuma lista sai `null`.",
        "tags": ["Coleta", "Análise"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "O caso, com o envio dos documentos e a análise.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Research"
                },
                "example": {
                  "id": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f",
                  "workspaceId": "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607",
                  "userId": "9c8b7a65-4321-4def-9876-0123456789ab",
                  "domain": "auto",
                  "objective": "sinistro",
                  "reportAddress": "Av. Paulista, 1000 - São Paulo/SP",
                  "reportTime": "2026-11-03 23:48",
                  "reportText": "Colisão traseira no cruzamento, sem vítimas.",
                  "doubts": ["O condutor estava autorizado a dirigir?"],
                  "status": "CREATED",
                  "incidentType": "colisao",
                  "vehicleCount": 1,
                  "entityCount": 2,
                  "documentCount": 7,
                  "documents": {
                    "status": "DONE",
                    "total": 7,
                    "progress": 100,
                    "errorCode": "",
                    "results": [
                      {
                        "index": 0,
                        "fileName": "bo.pdf",
                        "declaredType": "",
                        "type": "BOLETIM_OCORRENCIA",
                        "documentId": "b41c9f27-8d3e-4a16-9c5b-2e7f80a1d3c4",
                        "target": "BOLETIM_OCORRENCIA",
                        "position": "",
                        "outcome": "read",
                        "reason": ""
                      },
                      {
                        "index": 1,
                        "fileName": "frente.jpg",
                        "declaredType": "",
                        "type": "VEHICLE_FRENTE_CAPO_FAROIS",
                        "documentId": "c52da038-9e4f-4b27-ad6c-3f8091b2e4d5",
                        "target": "FOTO_VEICULO",
                        "position": "frente",
                        "outcome": "read",
                        "reason": ""
                      },
                      {
                        "index": 2,
                        "fileName": "selfie.jpg",
                        "declaredType": "",
                        "type": "",
                        "documentId": "",
                        "target": "OUTRO",
                        "position": "",
                        "outcome": "left_out",
                        "reason": "o arquivo não é um documento do caso"
                      }
                    ],
                    "assembly": {
                      "status": "DONE",
                      "errorCode": "",
                      "reason": "",
                      "documents": [
                        {
                          "documentId": "b41c9f27-8d3e-4a16-9c5b-2e7f80a1d3c4",
                          "outcome": "used",
                          "reason": ""
                        }
                      ],
                      "entities": [
                        {
                          "key": "VEHICLE:TST1A23",
                          "type": "VEHICLE",
                          "value": "TST1A23",
                          "name": "FIAT ARGO DRIVE 1.0",
                          "roles": ["PRINCIPAL"],
                          "entityId": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35",
                          "outcome": "created",
                          "reason": ""
                        },
                        {
                          "key": "PERSON:11144477735",
                          "type": "PERSON",
                          "value": "11144477735",
                          "name": "JOÃO DA SILVA",
                          "roles": ["SEGURADO", "MOTORISTA"],
                          "entityId": "a1b2c3d4-e5f6-4071-8293-a4b5c6d7e8f9",
                          "outcome": "created",
                          "reason": ""
                        }
                      ],
                      "institutional": 2,
                      "fields": ["incidentType", "reportAddress", "reportTime"],
                      "notes": ["A data do fato veio do boletim de ocorrência."]
                    }
                  },
                  "analysis": {
                    "status": "DONE",
                    "errorCode": "",
                    "startedAt": "2026-11-04T12:40:00Z",
                    "finishedAt": "2026-11-04T12:46:12Z",
                    "result": {
                      "dimdocumental": {
                        "status": "apurado",
                        "verdict": "atencao",
                        "reason": "",
                        "alerts": [
                          {
                            "code": "DOC_OWNER_MISMATCH",
                            "level": "atencao",
                            "entities": ["PERSON:11144477735"],
                            "documents": [
                              "b41c9f27-8d3e-4a16-9c5b-2e7f80a1d3c4"
                            ],
                            "evidence": [
                              "CRLV: MARIA DA SILVA",
                              "Apólice: JOÃO DA SILVA"
                            ],
                            "message": "O titular do CRLV não é o segurado da apólice."
                          }
                        ],
                        "summary": "Os documentos fecham entre si, com uma divergência de titularidade a esclarecer."
                      }
                    }
                  },
                  "kanbanId": "6b8e2f49-0a35-5c71-8d94-3e2f5a7b1c60",
                  "createdAt": "2026-11-04T12:00:00Z",
                  "updatedAt": "2026-11-04T12:46:12Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidResearchId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "$ref": "#/components/responses/ResearchNotFound"
          }
        }
      },
      "put": {
        "operationId": "updateResearch",
        "summary": "Editar o caso e movê-lo no quadro",
        "description": "Um **PATCH** do caso: o campo ausente (ou `null`) não muda, e string vazia não apaga. `status`, `userId`, `domain` e `workspaceId` não são campos editáveis — vindos no corpo, são ignorados.\n\n`kanbanId` **move o caso** para outra raia do quadro. A raia precisa ser uma linha `ACTIVE` do quadro deste workspace; qualquer outra coisa é `400`. Com o envio dos documentos ou a análise rodando, o movimento é `409`: naquele momento, o caso é do worker.\n\nÉ por aqui que o analista dá o desfecho: mover para **Aprovado** ou **Reprovado** (fase `DESFECHO`), ou devolver o caso para **Dados pendentes** quando falta material.",
        "tags": ["Coleta", "Investigação"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdPath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateResearchRequest"
              },
              "example": {
                "incidentType": "colisao",
                "reportText": "Colisão traseira no cruzamento, sem vítimas. O condutor é o filho do segurado.",
                "doubts": ["O condutor estava autorizado a dirigir?"],
                "kanbanId": "9d4a1c83-7e26-5f40-ab58-2c6d9e0f4a71"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "O caso depois da edição, já na raia nova.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResearchUpdated"
                }
              }
            }
          },
          "400": {
            "description": "Id inválido, ou a raia do `kanbanId` não é uma linha ativa do quadro deste workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "a raia não é do quadro deste workspace"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "$ref": "#/components/responses/ResearchNotFound"
          },
          "409": {
            "description": "O caso está com o envio dos documentos ou a análise em andamento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "o caso já tem um envio de documentos em andamento",
                  "code": "RESEARCH_DOCUMENTS_IN_PROGRESS"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "removeResearch",
        "summary": "Apagar um caso",
        "description": "Apaga o caso e tudo que pende dele. É irreversível, e só o dono do caso — o workspace do token — pode apagar.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdPath"
          }
        ],
        "responses": {
          "204": {
            "description": "O caso foi apagado. Sem corpo."
          },
          "400": {
            "$ref": "#/components/responses/InvalidResearchId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "$ref": "#/components/responses/ResearchNotFound"
          }
        }
      }
    },
    "/api/v1/lens/{workspaceId}/research/{researchId}/analyze": {
      "post": {
        "operationId": "requestResearchAnalysis",
        "summary": "Pedir a análise do caso",
        "description": "Põe a análise do caso na fila e responde `202` na hora. **Não tem corpo.**\n\nUm worker dentro da API enriquece o segurado e o terceiro (ZarvID e Asset) e roda as **nove dimensões** sobre o caso montado — os documentos, as leituras e os envolvidos. Cada dimensão grava o resultado dela assim que termina, então uma análise parcial já é legível. Isso leva minutos: nada disso cabe no tempo de uma requisição HTTP.\n\nO andamento e o resultado saem no `GET` do caso, em `analysis`. O caso vai para a raia **Analisando** no pedido, e para **Analisado** ou **Análise falhou** no fim.\n\n**Uma análise por vez.** Pedir outra com uma em andamento é `409`. Pedir com o envio dos documentos rodando também é `409`: a análise lê o caso que o envio ainda está gravando. Pedir uma análise num caso já analisado é permitido — ela substitui a anterior, e cada dimensão sobrescreve o campo dela.",
        "tags": ["Análise", "Investigação"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          }
        ],
        "responses": {
          "202": {
            "description": "A análise entrou na fila.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "researchId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "description": "Sempre `QUEUED` nesta resposta.",
                      "example": "QUEUED"
                    }
                  }
                },
                "example": {
                  "researchId": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f",
                  "status": "QUEUED"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidResearchId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "$ref": "#/components/responses/ResearchNotFound"
          },
          "409": {
            "description": "O caso já tem uma análise em andamento, ou o envio dos documentos ainda está rodando.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "analise": {
                    "summary": "Análise já em andamento",
                    "value": {
                      "error": "o caso já tem uma análise em andamento"
                    }
                  },
                  "envio": {
                    "summary": "Envio dos documentos em andamento",
                    "value": {
                      "error": "o caso já tem um envio de documentos em andamento",
                      "code": "RESEARCH_DOCUMENTS_IN_PROGRESS"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "A API subiu sem o agente de IA: não há quem analise.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "a análise está indisponível: a API subiu sem o agente"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/lens/{workspaceId}/research/{researchId}/document": {
      "post": {
        "operationId": "submitResearchDocuments",
        "summary": "Enviar os documentos do caso",
        "description": "Envia **todos os arquivos do caso de uma vez** e responde `202` sem chamar modelo nenhum. Os arquivos ficam na fila e um worker os identifica, lê e grava depois. É a rota que substituiu o antigo create de um arquivo por vez.\n\n### O que o worker faz com cada arquivo\n\n1. **Sem `type`**, o identificador de documento diz o que o arquivo é (`target`): apólice, BO, aviso de sinistro, termo de terceiro, CNH, CRLV, foto do local, foto do veículo ou `OUTRO`. O `OUTRO` fica fora do caso.\n2. **Foto de veículo** passa também pelo OCR Veículo, e a **posição** da foto (frente, traseira, lateral, interior, cofre do motor) é que dá o tipo do documento.\n3. **Com `agentReader: true`**, o OCR do tipo lê o arquivo e grava os campos extraídos no `metadata` do documento, na chave do agente.\n4. **Com `autoCreate: true`**, a leitura entra na **montagem do caso**: o agente de reconciliação transforma as leituras em envolvidos e preenche o relato (veja `documents.assembly` no `GET` do caso). `autoCreate` exige `agentReader` — sem leitura não há o que montar.\n\n### Limites\n\n| Limite | Valor |\n| --- | --- |\n| Arquivos por envio | 40 |\n| Tamanho de um arquivo (decodificado) | 10 MB |\n| Soma dos arquivos (decodificados) | 50 MB |\n| Formatos | `application/pdf`, `image/jpeg`, `image/png`, `image/webp` |\n\nO corpo inteiro é cortado em cerca de 67,7 MB antes de ser lido — o Base64 dos 50 MB mais folga para o resto do JSON. Passar disso é `413`.\n\n### Um envio por vez\n\nHá um envio por caso. Mandar outro com um em andamento é `409 RESEARCH_DOCUMENTS_IN_PROGRESS`. O andamento sai no `GET` do caso, em `documents`.\n\nUm arquivo recusado leva o **índice** dele no corpo do erro (`index`), para o front saber qual corrigir sem ler a frase.",
        "tags": ["Coleta", "Investigação"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitDocumentsRequest"
              },
              "examples": {
                "coleta": {
                  "summary": "Coleta inicial: o Lens identifica, lê e monta o caso",
                  "value": {
                    "files": [
                      {
                        "fileName": "bo.pdf",
                        "contentType": "application/pdf",
                        "base64": "JVBERi0xLjQKJeLjz9MK...",
                        "agentReader": true,
                        "autoCreate": true
                      },
                      {
                        "fileName": "apolice.pdf",
                        "contentType": "application/pdf",
                        "base64": "JVBERi0xLjQKJeLjz9MK...",
                        "agentReader": true,
                        "autoCreate": true
                      },
                      {
                        "fileName": "frente.jpg",
                        "contentType": "image/jpeg",
                        "base64": "/9j/4AAQSkZJRgABAQAA...",
                        "agentReader": true
                      }
                    ]
                  }
                },
                "tipado": {
                  "summary": "Arquivo com tipo declarado, sem agente",
                  "value": {
                    "researchEntityId": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35",
                    "files": [
                      {
                        "type": "VEHICLE_CRLV",
                        "fileName": "crlv.pdf",
                        "contentType": "application/pdf",
                        "base64": "JVBERi0xLjQKJeLjz9MK...",
                        "metadata": {
                          "origem": "upload do segurado"
                        }
                      }
                    ]
                  }
                },
                "diligencia": {
                  "summary": "Investigação: evidência que responde a um alerta",
                  "value": {
                    "files": [
                      {
                        "type": "TASK_EVIDENCE",
                        "fileName": "procuracao.pdf",
                        "contentType": "application/pdf",
                        "base64": "JVBERi0xLjQKJeLjz9MK...",
                        "metadata": {
                          "alerta": "DOC_OWNER_MISMATCH",
                          "nota": "Procuração do titular ao condutor."
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "O envio entrou na fila.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "researchId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "type": "string",
                      "description": "Sempre `QUEUED` nesta resposta.",
                      "example": "QUEUED"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Quantos arquivos entraram na fila.",
                      "example": 3
                    },
                    "progress": {
                      "type": "integer",
                      "description": "Sempre 0 nesta resposta.",
                      "example": 0
                    }
                  }
                },
                "example": {
                  "researchId": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f",
                  "status": "QUEUED",
                  "total": 3,
                  "progress": 0
                }
              }
            }
          },
          "400": {
            "description": "O envio ou um arquivo foi recusado. O erro de arquivo traz o `index` dele.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "semArquivo": {
                    "summary": "Envio vazio",
                    "value": {
                      "error": "o envio não tem arquivo"
                    }
                  },
                  "semTipoSemAgente": {
                    "summary": "Arquivo sem tipo e sem agentReader",
                    "value": {
                      "error": "files[2]: o arquivo sem tipo precisa do agentReader: é o agente que diz o tipo",
                      "index": 2
                    }
                  },
                  "formato": {
                    "summary": "Formato não aceito",
                    "value": {
                      "error": "files[0]: o content type tem de ser PDF, JPEG, PNG ou WEBP",
                      "index": 0
                    }
                  },
                  "autoCreate": {
                    "summary": "autoCreate sem agentReader",
                    "value": {
                      "error": "files[1]: o arquivo com o autoCreate precisa do agentReader: é a leitura que monta o caso",
                      "index": 1
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "$ref": "#/components/responses/ResearchNotFound"
          },
          "409": {
            "description": "O caso já tem um envio em andamento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "o caso já tem um envio de documentos em andamento",
                  "code": "RESEARCH_DOCUMENTS_IN_PROGRESS"
                }
              }
            }
          },
          "413": {
            "description": "Um arquivo passa de 10 MB, o envio passa de 40 arquivos ou de 50 MB, ou o corpo passou do teto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "arquivo": {
                    "summary": "Arquivo grande demais",
                    "value": {
                      "error": "files[3]: o arquivo passa de 10 MB",
                      "index": 3
                    }
                  },
                  "envio": {
                    "summary": "Envio grande demais",
                    "value": {
                      "error": "o envio passa de 50 MB"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "A API subiu sem o agente, e algum arquivo precisa dele (sem `type` ou com `agentReader`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "a API está sem o agente: o arquivo sem tipo ou com o agentReader não pode ser lido agora"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listResearchDocuments",
        "summary": "Listar os documentos do caso",
        "description": "Lista os documentos já gravados no caso, num array.\n\nUse **`?content=false`** sempre que não precisar do arquivo: a listagem leve devolve os mesmos itens sem o `base64`. Com 40 documentos de até 10 MB, a diferença entre as duas é de centenas de megabytes.\n\nO que os agentes leram de cada arquivo fica no `metadata`, na chave do agente.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          },
          {
            "name": "content",
            "in": "query",
            "required": false,
            "description": "`false` devolve a listagem leve, sem o `base64` de cada documento. Qualquer outro valor (ou a ausência) devolve o arquivo.",
            "schema": {
              "type": "string",
              "enum": ["false"],
              "example": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Os documentos do caso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Document"
                  }
                },
                "example": [
                  {
                    "id": "b41c9f27-8d3e-4a16-9c5b-2e7f80a1d3c4",
                    "researchId": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f",
                    "researchEntityId": "",
                    "type": "BOLETIM_OCORRENCIA",
                    "fileName": "bo.pdf",
                    "contentType": "application/pdf",
                    "base64": "",
                    "metadata": {
                      "identifyDocument": {
                        "target": "BOLETIM_OCORRENCIA",
                        "basis": "Cabeçalho da Polícia Civil e número de registro da ocorrência."
                      },
                      "ocrBo": {
                        "observed_kind": "BOLETIM_OCORRENCIA",
                        "report_number": "2026-0045321",
                        "occurrence_date": "2026-11-03",
                        "occurrence_city": "São Paulo",
                        "occurrence_state": "SP",
                        "declarant": "JOÃO DA SILVA",
                        "persons_cited": [
                          {
                            "name": "JOÃO DA SILVA",
                            "national_id": "11144477735",
                            "role_in_document": "condutor",
                            "vehicle_plate": "TST1A23"
                          }
                        ],
                        "vehicles_cited": [
                          {
                            "plate": "TST1A23",
                            "make": "FIAT",
                            "model": "ARGO",
                            "year": "2022",
                            "role_in_document": "veículo do declarante"
                          }
                        ]
                      },
                      "autoCreate": true
                    },
                    "createdAt": "2026-11-04T12:31:02Z",
                    "updatedAt": "2026-11-04T12:31:02Z"
                  }
                ]
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidResearchId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "$ref": "#/components/responses/ResearchNotFound"
          }
        }
      }
    },
    "/api/v1/lens/{workspaceId}/research/{researchId}/document/{id}": {
      "get": {
        "operationId": "getResearchDocument",
        "summary": "Consultar um documento",
        "description": "Um documento do caso, com o `base64` do arquivo e o `metadata` — onde ficam os campos que os OCRs extraíram, na chave de cada agente.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          },
          {
            "$ref": "#/components/parameters/DocumentId"
          }
        ],
        "responses": {
          "200": {
            "description": "O documento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidResearchId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "description": "O documento não está neste caso, ou o caso não existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "research document not found"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateResearchDocument",
        "summary": "Substituir um documento",
        "description": "Substitui o documento inteiro: `type`, `fileName`, `contentType` e `base64` são obrigatórios, e o que não vier no corpo é apagado. Não é um PATCH.\n\nÉ por aqui que se **corrige o tipo** que o agente errou, ou se amarra o documento a um envolvido pelo `researchEntityId`. O `metadata` é gravado como veio — para preservar o que os OCRs leram, releia o documento e reenvie o `metadata` inteiro com a sua chave a mais.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          },
          {
            "$ref": "#/components/parameters/DocumentId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateDocumentRequest"
              },
              "example": {
                "researchEntityId": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35",
                "type": "VEHICLE_CRLV",
                "fileName": "crlv.pdf",
                "contentType": "application/pdf",
                "base64": "JVBERi0xLjQKJeLjz9MK...",
                "metadata": {
                  "corrigidoPor": "analista"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "O documento depois da troca.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          },
          "400": {
            "description": "Falta um campo obrigatório, o tipo não é do vocabulário ou o `metadata` não é um objeto JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "type invalid"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "description": "O documento não está neste caso, ou o caso não existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "research document not found"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "removeResearchDocument",
        "summary": "Apagar um documento",
        "description": "Tira o documento do caso. O que o agente já leu dele continua gravado no resultado do envio e na análise que já rodou — apagar o arquivo não reescreve o passado.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          },
          {
            "$ref": "#/components/parameters/DocumentId"
          }
        ],
        "responses": {
          "204": {
            "description": "O documento foi apagado. Sem corpo."
          },
          "400": {
            "$ref": "#/components/responses/InvalidResearchId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "description": "O documento não está neste caso, ou o caso não existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "research document not found"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/lens/{workspaceId}/research/{researchId}/entity": {
      "post": {
        "operationId": "createResearchEntity",
        "summary": "Incluir um envolvido",
        "description": "Põe uma pessoa, uma empresa ou um veículo no caso, com os papéis dela.\n\n### A consulta do cadastro\n\nCriar um envolvido **consulta um provedor pago** e guarda o resultado em `profile`:\n\n| Tipo | `value` | Consulta | Chave em `profile` |\n| --- | --- | --- | --- |\n| `PERSON` | CPF | Dataset do Zarv ID v3 (PF) | `dataset` |\n| `COMPANY` | CNPJ | Dataset do Zarv ID v3 (PJ) | `dataset` |\n| `VEHICLE` | Placa | Zarv Asset | `asset` |\n\nO CPF ou o CNPJ é conferido (tamanho e dígito verificador) **antes** da consulta: um erro de digitação é `400`, e não uma consulta cobrada. A consulta vem depois de todas as conferências e antes de gravar — o que seria recusado não é cobrado, e uma consulta que falha não grava nada (`502`).\n\n### As regras da árvore\n\n- O `VEHICLE` é sempre raiz: ele não tem `parentId`.\n- A `COMPANY` só fica sob um `VEHICLE`.\n- O `parentId` tem de ser um envolvido **deste** caso.\n\n### Duas regras que evitam caso duplicado\n\n- **Dentro do caso:** se já existe um envolvido com esse `value`, a chamada devolve o que já está lá, sem gravar de novo e sem consultar.\n- **Entre casos:** uma placa que já está num caso **em aberto do mesmo workspace** é recusada com `409 VEHICLE_IN_OPEN_RESEARCH`, e o corpo traz o `researchId` daquele caso — é para onde as evidências devem ir.\n\n`value` é obrigatório no `VEHICLE` (a placa). Em `PERSON` e `COMPANY` o documento é opcional: uma testemunha pode não ter.\n\nO envolvido criado por aqui nasce `manual: true` — foi **declarado**, não descoberto. O que a montagem do caso cria a partir das leituras nasce `manual: false`.",
        "tags": ["Coleta", "Investigação"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateEntityRequest"
              },
              "examples": {
                "veiculo": {
                  "summary": "O veículo do sinistro",
                  "value": {
                    "type": "VEHICLE",
                    "value": "TST1A23",
                    "roles": ["PRINCIPAL"],
                    "reason": "Veículo segurado, declarado na abertura."
                  }
                },
                "segurado": {
                  "summary": "O segurado, que também dirigia",
                  "value": {
                    "type": "PERSON",
                    "value": "111.444.777-35",
                    "roles": ["SEGURADO", "MOTORISTA"],
                    "parentId": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35"
                  }
                },
                "oficina": {
                  "summary": "A oficina que atendeu o veículo",
                  "value": {
                    "type": "COMPANY",
                    "value": "11.222.333/0001-81",
                    "roles": ["FUNILARIA"],
                    "parentId": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35",
                    "reason": "Orçamento apresentado pelo segurado."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "O envolvido entrou no caso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                },
                "example": {
                  "id": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35",
                  "researchId": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f",
                  "parentId": "",
                  "externalId": "",
                  "type": "VEHICLE",
                  "value": "TST1A23",
                  "roles": ["PRINCIPAL"],
                  "reason": "Veículo segurado, declarado na abertura.",
                  "profile": {
                    "asset": {
                      "licensePlate": "TST1A23",
                      "brand": "FIAT",
                      "model": "ARGO DRIVE 1.0",
                      "modelYear": 2022
                    }
                  },
                  "manual": true,
                  "metadata": {},
                  "createdAt": "2026-11-04T12:05:00Z",
                  "updatedAt": "2026-11-04T12:05:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Tipo ou papel inválido, papel de empresa numa pessoa física, CPF/CNPJ inválido, `profile` que não é objeto, pai que a árvore não aceita, ou `reason` acima de 2.000 caracteres.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "papel": {
                    "summary": "Papel de empresa numa pessoa física",
                    "value": {
                      "error": "role is only valid for a company",
                      "code": "RESEARCH_ENTITY_ROLE_TYPE_MISMATCH"
                    }
                  },
                  "documento": {
                    "summary": "CPF ou CNPJ inválido",
                    "value": {
                      "error": "o CPF ou o CNPJ não é válido para a consulta"
                    }
                  },
                  "placa": {
                    "summary": "Veículo sem placa",
                    "value": {
                      "error": "a placa (value) é obrigatória no veículo"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "description": "O caso não existe, ou o `parentId` não está neste caso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "o pai não está neste caso"
                }
              }
            }
          },
          "409": {
            "description": "A placa já está num caso em aberto deste workspace. O corpo traz o caso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "a placa TST1A23 já está na pesquisa 7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f, que ainda está em aberto",
                  "code": "VEHICLE_IN_OPEN_RESEARCH",
                  "researchId": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f",
                  "licensePlate": "TST1A23"
                }
              }
            }
          },
          "502": {
            "description": "A consulta do cadastro falhou (provedor fora do ar, tempo esgotado, produto não contratado). Nada foi gravado — repita.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "a consulta do profile falhou, e nada foi gravado",
                  "code": "RESEARCH_ENTITY_PROFILE_LOOKUP_FAILED"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listResearchEntities",
        "summary": "Listar os envolvidos do caso",
        "description": "Todos os envolvidos do caso, num array: as pessoas, as empresas e os veículos, cada um com os papéis, o cadastro consultado (`profile`) e o `parentId` que forma a árvore.\n\n`manual` diz quem o pôs ali: `true` é declaração de quem abriu o caso; `false` é achado da apuração — a diferença é de peso probatório.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          }
        ],
        "responses": {
          "200": {
            "description": "Os envolvidos do caso.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Entity"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidResearchId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "$ref": "#/components/responses/ResearchNotFound"
          }
        }
      }
    },
    "/api/v1/lens/{workspaceId}/research/{researchId}/entity/{id}": {
      "get": {
        "operationId": "getResearchEntity",
        "summary": "Consultar um envolvido",
        "description": "Um envolvido do caso, com os papéis, o cadastro consultado e o vínculo com o veículo.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          },
          {
            "$ref": "#/components/parameters/EntityId"
          }
        ],
        "responses": {
          "200": {
            "description": "O envolvido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidResearchId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "description": "O envolvido não está neste caso, ou o caso não existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "research entity not found"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateResearchEntity",
        "summary": "Editar um envolvido",
        "description": "Um **PATCH**: muda os papéis, o motivo, o cadastro, os metadados e o vínculo com um veículo do caso.\n\n- `parentId`: `null` não mexe, `\"\"` desliga o vínculo, um id amarra ao veículo.\n- `reason`: `null` não mexe, `\"\"` apaga.\n- `value` **é imutável**. Se vier, tem de ser exatamente o que está gravado; qualquer outro valor é `400 IDENTITY_IS_IMMUTABLE`. A placa e o CPF/CNPJ são a chave pela qual o Asset, o Zarv ID e as dimensões foram consultados — corrigir é remover e incluir de novo.\n- `externalId`, `type` e `manual` não são editáveis e são ignorados no corpo.",
        "tags": ["Coleta", "Investigação"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          },
          {
            "$ref": "#/components/parameters/EntityId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateEntityRequest"
              },
              "example": {
                "roles": ["SEGURADO"],
                "reason": "A apuração mostrou que quem dirigia era o filho.",
                "parentId": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "O envolvido depois da edição.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entity"
                }
              }
            }
          },
          "400": {
            "description": "Papel inválido, papel de empresa numa pessoa física, `value` diferente do gravado, pai não permitido ou ciclo na árvore.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "identidade": {
                    "summary": "Tentou trocar a placa ou o documento",
                    "value": {
                      "error": "identity is immutable",
                      "code": "IDENTITY_IS_IMMUTABLE"
                    }
                  },
                  "ciclo": {
                    "summary": "O pai novo fecharia um ciclo",
                    "value": {
                      "error": "o pai novo fecharia um ciclo na árvore do caso"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "description": "O envolvido não está neste caso, ou o `parentId` não está no caso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "research entity not found"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "removeResearchEntity",
        "summary": "Remover um envolvido",
        "description": "Tira o envolvido do caso. Remover e incluir de novo é também como se corrige uma placa ou um CPF digitado errado — e a inclusão nova **consulta o cadastro outra vez**, com a cobrança que vem com ela.",
        "tags": ["Coleta"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "$ref": "#/components/parameters/ResearchIdParam"
          },
          {
            "$ref": "#/components/parameters/EntityId"
          }
        ],
        "responses": {
          "204": {
            "description": "O envolvido foi removido. Sem corpo."
          },
          "400": {
            "$ref": "#/components/responses/InvalidResearchId"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          },
          "404": {
            "description": "O envolvido não está neste caso, ou o caso não existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "research entity not found"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/lens/{workspaceId}/kanban": {
      "get": {
        "operationId": "listKanbanLanes",
        "summary": "Listar as raias do quadro",
        "description": "As raias do quadro do workspace, num array na ordem do quadro: primeiro a fase (`COLETA`, `ANALISE`, `DESFECHO`), depois o `order` dentro da fase e, no empate, a criação. Não há paginação — o quadro tem poucas raias.\n\n**O quadro nasce na primeira leitura.** Um workspace que nunca leu o quadro recebe as **nove raias padrão** gravadas nesta chamada, com o autor `system`:\n\n| Fase | Raias |\n| --- | --- |\n| `COLETA` | Abertura · Dados pendentes · Dados inconsistentes · Pronto para análise |\n| `ANALISE` | Analisando · Análise falhou · Analisado |\n| `DESFECHO` | Reprovado · Aprovado |\n\nOs ids das nove são estáveis: derivam do workspace, da fase e do nome, então são os mesmos em toda leitura. Duas chamadas simultâneas gravam cada raia uma única vez.\n\nUse o `id` da raia no `?kanbanId=` da listagem de casos para montar cada coluna, e no `kanbanId` do `PUT` do caso para mover um caso.\n\nRaias `INACTIVE` continuam na lista: esconder é decisão do cliente.",
        "tags": ["Quadro"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          }
        ],
        "responses": {
          "200": {
            "description": "As raias do quadro, na ordem.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/KanbanLane"
                  }
                },
                "example": [
                  {
                    "id": "0f3b5d21-9c47-5e8a-b6d2-1a4c7e9f03b8",
                    "workspaceId": "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607",
                    "userId": "system",
                    "phase": "COLETA",
                    "step": "Abertura",
                    "description": "",
                    "order": 1,
                    "status": "ACTIVE",
                    "createdAt": "2026-11-04T11:59:00Z",
                    "updatedAt": "2026-11-04T11:59:00Z"
                  },
                  {
                    "id": "9d4a1c83-7e26-5f40-ab58-2c6d9e0f4a71",
                    "workspaceId": "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607",
                    "userId": "system",
                    "phase": "COLETA",
                    "step": "Dados pendentes",
                    "description": "",
                    "order": 2,
                    "status": "ACTIVE",
                    "createdAt": "2026-11-04T11:59:00Z",
                    "updatedAt": "2026-11-04T11:59:00Z"
                  },
                  {
                    "id": "6b8e2f49-0a35-5c71-8d94-3e2f5a7b1c60",
                    "workspaceId": "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607",
                    "userId": "system",
                    "phase": "ANALISE",
                    "step": "Analisando",
                    "description": "",
                    "order": 1,
                    "status": "ACTIVE",
                    "createdAt": "2026-11-04T11:59:00Z",
                    "updatedAt": "2026-11-04T11:59:00Z"
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NotOwner"
          }
        }
      }
    },
    "/api/v1/lens/{workspaceId}/kanban/{id}": {
      "get": {
        "operationId": "getKanbanLane",
        "summary": "Consultar uma raia",
        "description": "Uma raia do quadro do workspace. Uma raia de outro workspace responde `403`, como um caso de outro workspace.",
        "tags": ["Quadro"],
        "parameters": [
          {
            "$ref": "#/components/parameters/WorkspaceId"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "O id da raia.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A raia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KanbanLane"
                }
              }
            }
          },
          "400": {
            "description": "O id não é um UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "o id da coluna não é um UUID válido"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "A raia é de outro workspace, ou o token não é do workspace do caminho.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "a coluna é de outro workspace"
                }
              }
            }
          },
          "404": {
            "description": "A raia não existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "coluna do kanban não encontrada"
                }
              }
            }
          }
        }
      }
    },
    "/api/lens/health": {
      "get": {
        "operationId": "getLensHealth",
        "summary": "Saúde do serviço",
        "description": "Diz se o Lens está no ar e enxergando o banco. É a única rota sem autenticação.",
        "tags": ["Operação"],
        "security": [],
        "responses": {
          "200": {
            "description": "O serviço está no ar.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    }
                  }
                },
                "example": {
                  "status": "ok"
                }
              }
            }
          },
          "503": {
            "description": "O serviço está no ar, mas sem o banco.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "unavailable"
                    }
                  }
                },
                "example": {
                  "status": "unavailable"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Token Bearer JWT obtido em `POST /api/v1/authentication`. O workspace do token precisa ser o mesmo do caminho."
      }
    },
    "parameters": {
      "WorkspaceId": {
        "name": "workspaceId",
        "in": "path",
        "required": true,
        "description": "O workspace dono do caso. Tem de ser o workspace do token.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607"
      },
      "ResearchIdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "O id do caso.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f"
      },
      "ResearchIdParam": {
        "name": "researchId",
        "in": "path",
        "required": true,
        "description": "O id do caso.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "7d2f8e10-4c3b-4a5d-9e8f-1a2b3c4d5e6f"
      },
      "DocumentId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "O id do documento.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "b41c9f27-8d3e-4a16-9c5b-2e7f80a1d3c4"
      },
      "EntityId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "O id do envolvido.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35"
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Chave única por caso (um UUID, por exemplo). Repetir a requisição com a mesma chave e o mesmo corpo devolve o caso que já existe, com `200` e `Idempotent-Replayed: true`.",
        "schema": {
          "type": "string"
        },
        "example": "6f1c2a9e-4b7d-4e2a-9c3f-0a1b2c3d4e5f"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Token ausente, inválido ou expirado.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "authenticated user is required"
            }
          }
        }
      },
      "NotOwner": {
        "description": "O token não é do workspace do caminho, ou o caso é de outro workspace.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "research not owner"
            }
          }
        }
      },
      "InvalidResearchId": {
        "description": "O id do caso não é um UUID.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "research id is invalid"
            }
          }
        }
      },
      "ResearchNotFound": {
        "description": "O caso não existe.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "research not found"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "O corpo de toda recusa. `error` é a frase de diagnóstico e pode mudar; decida pelo `code`, que é estável, quando a recusa tiver um.",
        "properties": {
          "error": {
            "type": "string",
            "description": "A frase da recusa.",
            "example": "research not found"
          },
          "code": {
            "type": "string",
            "description": "Código estável, nas recusas que têm um.",
            "enum": [
              "IDEMPOTENCY_KEY_REUSED",
              "RESEARCH_ENTITY_ROLE_TYPE_MISMATCH",
              "RESEARCH_ENTITY_PROFILE_LOOKUP_FAILED",
              "RESEARCH_DOCUMENTS_IN_PROGRESS",
              "IDENTITY_IS_IMMUTABLE",
              "VEHICLE_IN_OPEN_RESEARCH"
            ]
          },
          "index": {
            "type": "integer",
            "description": "Só quando um arquivo do envio foi recusado: a posição dele em `files`.",
            "example": 2
          },
          "researchId": {
            "type": "string",
            "format": "uuid",
            "description": "Só em `VEHICLE_IN_OPEN_RESEARCH`: o caso em aberto que já tem a placa."
          },
          "licensePlate": {
            "type": "string",
            "description": "Só em `VEHICLE_IN_OPEN_RESEARCH`: a placa recusada."
          }
        },
        "required": ["error"]
      },
      "ResearchStatus": {
        "type": "string",
        "description": "O status do caso.",
        "enum": ["CREATED", "PROCESSING", "COMPLETED", "FAILED"],
        "example": "CREATED"
      },
      "IncidentType": {
        "type": "string",
        "description": "O tipo da ocorrência. Decide quais dimensões rodam na análise: posse só em roubo e furto, croqui só em colisão. Vazio deixa o fluxo deduzir do relato.",
        "enum": [
          "colisao",
          "roubo",
          "furto",
          "desvio_de_carga",
          "coleta_nao_autorizada",
          "divergencia_operacional"
        ],
        "example": "colisao"
      },
      "DocumentType": {
        "type": "string",
        "description": "O tipo do documento. Os seis primeiros têm OCR próprio; as fotos do veículo saem da posição que o OCR Veículo lê.",
        "enum": [
          "BOLETIM_OCORRENCIA",
          "APOLICE",
          "AVISO_SINISTRO",
          "TERMO_TERCEIRO",
          "VEHICLE_CNH_CONDUTOR",
          "VEHICLE_CRLV",
          "VEHICLE_CONTRATO_LOCACAO",
          "VEHICLE_FRENTE_CAPO_FAROIS",
          "VEHICLE_LATERAL_ESQUERDA",
          "VEHICLE_LATERAL_DIREITA",
          "VEHICLE_TRASEIRA",
          "VEHICLE_INTERIOR_AIRBAGS",
          "VEHICLE_COFRE_MOTOR",
          "DOCUMENTO_PROPRIETARIO",
          "DOCUMENTO_CONDUTOR",
          "DOCUMENTO_PASSAGEIRO",
          "DOCUMENTO_VITIMA",
          "DOCUMENTO_TESTEMUNHA",
          "LOCAL_FOTO",
          "TASK_EVIDENCE",
          "TELEMETRIA"
        ],
        "example": "BOLETIM_OCORRENCIA"
      },
      "EntityType": {
        "type": "string",
        "description": "O que o envolvido é.",
        "enum": ["PERSON", "COMPANY", "VEHICLE"],
        "example": "PERSON"
      },
      "EntityRole": {
        "type": "string",
        "description": "O papel do envolvido no caso. `PRINCIPAL`, `TERCEIRO_ENVOLVIDO`, `CADASTRADO_CONTA`, `ALOCADO_PEDIDO` e `OBSERVADO_COLETA` são do veículo; `FUNILARIA`, `MECANICA` e `LOCADORA` só valem em `COMPANY`.",
        "enum": [
          "PRINCIPAL",
          "TERCEIRO_ENVOLVIDO",
          "CADASTRADO_CONTA",
          "ALOCADO_PEDIDO",
          "OBSERVADO_COLETA",
          "SEGURADO",
          "DONO",
          "MOTORISTA",
          "PASSAGEIRO",
          "VITIMA",
          "TESTEMUNHA",
          "MOTORISTA_ALOCADO",
          "MOTORISTA_OBSERVADO",
          "CRIMINOSO",
          "REDE_FAMILIAR",
          "REDE_FAMILIAR_PAI",
          "REDE_FAMILIAR_MAE",
          "REDE_FAMILIAR_FILHO",
          "REDE_FAMILIAR_IRMAO",
          "REDE_FAMILIAR_CONJUGE",
          "REDE_SOCIO",
          "REDE_REU_JUNTO",
          "FUNILARIA",
          "MECANICA",
          "LOCADORA"
        ],
        "example": "SEGURADO"
      },
      "JobStatus": {
        "type": "string",
        "description": "O estado de um trabalho assíncrono. Vazio quando ele nunca rodou neste caso.",
        "enum": ["", "QUEUED", "PROCESSING", "DONE", "FAILED"],
        "example": "DONE"
      },
      "CreateResearchRequest": {
        "type": "object",
        "required": ["domain", "objective", "status"],
        "properties": {
          "domain": {
            "type": "string",
            "description": "O domínio do caso. Em seguro auto, `auto`.",
            "example": "auto"
          },
          "objective": {
            "type": "string",
            "description": "O que se quer do caso.",
            "example": "sinistro"
          },
          "status": {
            "$ref": "#/components/schemas/ResearchStatus"
          },
          "incidentType": {
            "$ref": "#/components/schemas/IncidentType"
          },
          "reportAddress": {
            "type": "string",
            "description": "Onde o fato aconteceu, como declarado.",
            "example": "Av. Paulista, 1000 - São Paulo/SP"
          },
          "reportTime": {
            "type": "string",
            "format": "date-time",
            "description": "Quando o fato aconteceu, em RFC3339. Mande com o fuso do lugar do fato.",
            "example": "2026-11-03T23:48:00-03:00"
          },
          "reportText": {
            "type": "string",
            "description": "O relato, em texto livre.",
            "example": "Colisão traseira no cruzamento, sem vítimas."
          },
          "doubts": {
            "type": "array",
            "description": "As perguntas que o caso precisa responder.",
            "items": {
              "type": "string"
            },
            "example": ["O condutor estava autorizado a dirigir?"]
          },
          "bot": {
            "type": "object",
            "description": "Envelope livre da sessão que abriu o caso, quando ele veio por um conector: canal, conversa, bot, locale, contato. Guardado como veio.",
            "additionalProperties": true
          },
          "userId": {
            "type": "string",
            "description": "Ignorado. O autor é o usuário do token.",
            "deprecated": true
          }
        }
      },
      "UpdateResearchRequest": {
        "type": "object",
        "description": "Um PATCH: o campo ausente ou `null` não muda. `status`, `userId`, `domain` e `workspaceId` não são editáveis.",
        "properties": {
          "objective": {
            "type": "string",
            "example": "sinistro"
          },
          "incidentType": {
            "$ref": "#/components/schemas/IncidentType"
          },
          "reportAddress": {
            "type": "string"
          },
          "reportTime": {
            "type": "string",
            "format": "date-time"
          },
          "reportText": {
            "type": "string"
          },
          "doubts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "kanbanId": {
            "type": "string",
            "format": "uuid",
            "description": "Move o caso para esta raia. Tem de ser uma raia `ACTIVE` do quadro deste workspace.",
            "example": "9d4a1c83-7e26-5f40-ab58-2c6d9e0f4a71"
          }
        }
      },
      "ResearchBase": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "workspaceId": {
            "type": "string",
            "format": "uuid"
          },
          "userId": {
            "type": "string",
            "format": "uuid",
            "description": "Quem abriu o caso."
          },
          "domain": {
            "type": "string",
            "example": "auto"
          },
          "objective": {
            "type": "string",
            "example": "sinistro"
          },
          "reportAddress": {
            "type": "string"
          },
          "reportText": {
            "type": "string"
          },
          "doubts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "status": {
            "$ref": "#/components/schemas/ResearchStatus"
          },
          "incidentType": {
            "$ref": "#/components/schemas/IncidentType"
          },
          "bot": {
            "type": "object",
            "additionalProperties": true
          },
          "kanbanId": {
            "type": "string",
            "format": "uuid",
            "description": "A raia do caso no quadro."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ResearchCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ResearchBase"
          },
          {
            "type": "object",
            "properties": {
              "reportTime": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "O instante do fato, como foi gravado."
              }
            }
          }
        ]
      },
      "ResearchUpdated": {
        "$ref": "#/components/schemas/ResearchCreated"
      },
      "ResearchListItem": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ResearchBase"
          },
          {
            "type": "object",
            "properties": {
              "reportTime": {
                "type": "string",
                "description": "A data do fato **como declarada**: o tempo civil quando existe (`2026-11-03 23:48`), e o instante em RFC3339 nos casos gravados antes disso. Nunca convertida.",
                "example": "2026-11-03 23:48"
              },
              "vehicleCount": {
                "type": "integer",
                "description": "Quantos veículos o caso tem.",
                "example": 1
              },
              "entityCount": {
                "type": "integer",
                "description": "Quantas pessoas e empresas o caso tem.",
                "example": 2
              },
              "documentCount": {
                "type": "integer",
                "description": "Quantos documentos o caso tem.",
                "example": 7
              }
            }
          }
        ]
      },
      "Research": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ResearchListItem"
          },
          {
            "type": "object",
            "properties": {
              "documents": {
                "$ref": "#/components/schemas/DocumentsJob"
              },
              "analysis": {
                "$ref": "#/components/schemas/AnalysisJob"
              }
            }
          }
        ]
      },
      "ResearchPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResearchListItem"
            }
          },
          "total": {
            "type": "integer",
            "description": "Quantos casos atendem ao filtro, antes da paginação.",
            "example": 42
          }
        }
      },
      "UserResearchPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResearchBase"
            }
          },
          "total": {
            "type": "integer",
            "example": 7
          }
        }
      },
      "DocumentsJob": {
        "type": "object",
        "description": "O envio dos documentos do caso. Caso sem envio tem a mesma forma, vazia.",
        "properties": {
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "total": {
            "type": "integer",
            "description": "Quantos arquivos o envio tem.",
            "example": 7
          },
          "progress": {
            "type": "integer",
            "description": "De 0 a 100.",
            "example": 100
          },
          "errorCode": {
            "type": "string",
            "description": "`ATTEMPTS_EXHAUSTED` quando o envio derrubou o worker três vezes. Vazio quando não falhou.",
            "example": ""
          },
          "results": {
            "type": "array",
            "description": "O que o worker fez com cada arquivo, na ordem em que terminou.",
            "items": {
              "$ref": "#/components/schemas/DocumentResult"
            }
          },
          "assembly": {
            "$ref": "#/components/schemas/Assembly"
          }
        }
      },
      "DocumentResult": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "A posição do arquivo em `files`.",
            "example": 0
          },
          "fileName": {
            "type": "string",
            "example": "bo.pdf"
          },
          "declaredType": {
            "type": "string",
            "description": "O tipo que veio no envio. Vazio quando o arquivo foi enviado sem tipo.",
            "example": ""
          },
          "type": {
            "type": "string",
            "description": "O tipo final do documento, depois dos agentes.",
            "example": "BOLETIM_OCORRENCIA"
          },
          "documentId": {
            "type": "string",
            "description": "O documento gravado. Vazio quando o arquivo ficou fora do caso.",
            "example": "b41c9f27-8d3e-4a16-9c5b-2e7f80a1d3c4"
          },
          "target": {
            "type": "string",
            "description": "O que o identificador de documento disse que o arquivo é.",
            "enum": [
              "",
              "APOLICE",
              "BOLETIM_OCORRENCIA",
              "AVISO_SINISTRO",
              "TERMO_TERCEIRO",
              "CNH",
              "CRLV",
              "FOTO_LOCAL",
              "FOTO_VEICULO",
              "OUTRO"
            ],
            "example": "BOLETIM_OCORRENCIA"
          },
          "position": {
            "type": "string",
            "description": "A posição da foto, lida pelo OCR Veículo. Só em foto de veículo.",
            "enum": [
              "",
              "frente",
              "traseira",
              "lateral_esquerda",
              "lateral_direita",
              "interior",
              "cofre_motor",
              "outro",
              "nao_e_veiculo"
            ],
            "example": ""
          },
          "outcome": {
            "type": "string",
            "description": "`saved`: gravado sem leitura. `read`: gravado com a leitura do agente. `read_failed`: gravado, mas o agente não conseguiu ler. `left_out`: ficou fora do caso, e o `reason` diz por quê.",
            "enum": ["saved", "read", "read_failed", "left_out"],
            "example": "read"
          },
          "reason": {
            "type": "string",
            "description": "Por que o arquivo ficou fora, ou por que a leitura falhou.",
            "example": ""
          }
        }
      },
      "Assembly": {
        "type": "object",
        "description": "A montagem do caso no fim do envio: o agente de reconciliação lê os documentos com `autoCreate`, resolve quem é quem e grava os envolvidos. O envio que não montou nada tem a mesma forma, vazia.",
        "properties": {
          "status": {
            "type": "string",
            "enum": ["", "DONE", "FAILED"],
            "example": "DONE"
          },
          "errorCode": {
            "type": "string",
            "description": "`RECONCILIATION_FAILED` quando o agente não respondeu; `AGENT_UNAVAILABLE` quando a API subiu sem ele. A falha da montagem não falha o envio.",
            "enum": ["", "RECONCILIATION_FAILED", "AGENT_UNAVAILABLE"],
            "example": ""
          },
          "reason": {
            "type": "string",
            "example": ""
          },
          "documents": {
            "type": "array",
            "description": "Os documentos com `autoCreate` e o que a montagem fez com cada um.",
            "items": {
              "type": "object",
              "properties": {
                "documentId": {
                  "type": "string",
                  "format": "uuid"
                },
                "outcome": {
                  "type": "string",
                  "enum": ["used", "left_out"],
                  "example": "used"
                },
                "reason": {
                  "type": "string"
                }
              }
            }
          },
          "entities": {
            "type": "array",
            "description": "Cada envolvido que a reconciliação encontrou e o que a montagem fez com ele.",
            "items": {
              "$ref": "#/components/schemas/AssembledEntity"
            }
          },
          "institutional": {
            "type": "integer",
            "description": "Quantas citações eram da parte institucional dos documentos (cabeçalho, carimbo, órgão emissor) e por isso não viraram envolvido.",
            "example": 2
          },
          "fields": {
            "type": "array",
            "description": "Os campos do caso que a montagem preencheu ou substituiu.",
            "items": {
              "type": "string"
            },
            "example": ["incidentType", "reportAddress", "reportTime"]
          },
          "notes": {
            "type": "array",
            "description": "As notas da reconciliação.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "AssembledEntity": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "A chave da entidade na reconciliação: a placa (só letras e dígitos) ou o CPF/CNPJ (só dígitos).",
            "example": "VEHICLE:TST1A23"
          },
          "type": {
            "$ref": "#/components/schemas/EntityType"
          },
          "value": {
            "type": "string",
            "example": "TST1A23"
          },
          "name": {
            "type": "string",
            "example": "FIAT ARGO DRIVE 1.0"
          },
          "roles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntityRole"
            }
          },
          "entityId": {
            "type": "string",
            "description": "O envolvido do caso. Vazio quando ficou de fora.",
            "example": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35"
          },
          "outcome": {
            "type": "string",
            "description": "`created`: nasceu agora. `updated`: já estava no caso e ganhou papéis ou o vínculo. `unchanged`: já estava, sem nada a mudar. `left_out`: não entrou, e o `reason` diz por quê — pessoa ou empresa sem CPF/CNPJ, veículo sem placa, ou veículo cuja placa está em outro caso aberto.",
            "enum": ["created", "updated", "unchanged", "left_out"],
            "example": "created"
          },
          "reason": {
            "type": "string",
            "example": ""
          }
        }
      },
      "AnalysisJob": {
        "type": "object",
        "description": "A análise do caso. Caso sem análise tem `status` vazio e `result` nulo.",
        "properties": {
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "errorCode": {
            "type": "string",
            "description": "`DIMENSIONS_FAILED`: alguma dimensão falhou, e o resultado das outras está gravado. `ANALYSIS_FAILED`: a análise parou por outro motivo (caso sem documento, envolvido inválido, banco). `ATTEMPTS_EXHAUSTED`: a análise derrubou o worker três vezes e não volta à fila.",
            "enum": [
              "",
              "DIMENSIONS_FAILED",
              "ANALYSIS_FAILED",
              "ATTEMPTS_EXHAUSTED"
            ],
            "example": ""
          },
          "startedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "finishedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "result": {
            "$ref": "#/components/schemas/AnalysisResult"
          }
        }
      },
      "AnalysisResult": {
        "type": "object",
        "nullable": true,
        "description": "Um campo por dimensão, gravado assim que ela termina. Uma análise que falhou numa dimensão tem o resultado das outras.",
        "properties": {
          "dimdocumental": {
            "$ref": "#/components/schemas/Dimension"
          },
          "dimchronology": {
            "$ref": "#/components/schemas/Dimension"
          },
          "dimcoverage": {
            "$ref": "#/components/schemas/Dimension"
          },
          "dimpossession": {
            "$ref": "#/components/schemas/Dimension"
          },
          "dimimagery": {
            "$ref": "#/components/schemas/Dimension"
          },
          "dimterritory": {
            "$ref": "#/components/schemas/Dimension"
          },
          "dimeconomic": {
            "$ref": "#/components/schemas/Dimension"
          },
          "dimnetwork": {
            "$ref": "#/components/schemas/Dimension"
          },
          "dimphysical": {
            "$ref": "#/components/schemas/Dimension"
          }
        }
      },
      "Dimension": {
        "type": "object",
        "description": "O resultado de uma dimensão. O veredito é o **pior nível** entre os alertas: um crítico basta para o veredito ser crítico.",
        "properties": {
          "status": {
            "type": "string",
            "description": "`apurado`: a dimensão tinha o que cruzar e cruzou. `nao_apurado`: faltou dado, fonte ou parte — e o `reason` diz o quê. Não apurar é resposta honesta, não erro.",
            "enum": ["apurado", "nao_apurado"],
            "example": "apurado"
          },
          "verdict": {
            "type": "string",
            "description": "`critico`: o apurado contraria o relato. `atencao`: há ponto a esclarecer. `compativel`: o apurado sustenta o relato. Vazio quando a dimensão não apurou.",
            "enum": ["critico", "atencao", "compativel"],
            "example": "atencao"
          },
          "reason": {
            "type": "string",
            "description": "Por que a dimensão não apurou. Vazio quando apurou.",
            "example": ""
          },
          "alerts": {
            "type": "array",
            "description": "Os achados, do mais grave ao informativo. Saem sempre do código, nunca do modelo.",
            "items": {
              "$ref": "#/components/schemas/Alert"
            }
          },
          "summary": {
            "type": "string",
            "description": "A leitura do caso em uma frase — a única parte escrita pelo modelo.",
            "example": "Os documentos fecham entre si, com uma divergência de titularidade a esclarecer."
          }
        }
      },
      "Alert": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "O código do achado, no vocabulário fechado da dimensão.",
            "example": "DOC_OWNER_MISMATCH"
          },
          "level": {
            "type": "string",
            "enum": ["critico", "atencao", "compativel"],
            "example": "atencao"
          },
          "entities": {
            "type": "array",
            "description": "As chaves dos envolvidos que o alerta aponta.",
            "items": {
              "type": "string"
            },
            "example": ["PERSON:11144477735"]
          },
          "documents": {
            "type": "array",
            "description": "Os ids dos documentos que sustentam o alerta.",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "evidence": {
            "type": "array",
            "description": "O que sustenta o achado, em texto curto: a data, a placa, o nome.",
            "items": {
              "type": "string"
            },
            "example": ["CRLV: MARIA DA SILVA", "Apólice: JOÃO DA SILVA"]
          },
          "message": {
            "type": "string",
            "description": "A frase que o analista lê.",
            "example": "O titular do CRLV não é o segurado da apólice."
          }
        }
      },
      "SubmitDocumentsRequest": {
        "type": "object",
        "required": ["files"],
        "properties": {
          "researchEntityId": {
            "type": "string",
            "format": "uuid",
            "description": "Amarra **todos** os arquivos do envio a este envolvido. Opcional.",
            "example": "5e9a3c71-2b4d-4e8f-a9c0-6d1b7f2e8a35"
          },
          "files": {
            "type": "array",
            "description": "Os arquivos do caso. De 1 a 40.",
            "minItems": 1,
            "maxItems": 40,
            "items": {
              "$ref": "#/components/schemas/SubmitFile"
            }
          }
        }
      },
      "SubmitFile": {
        "type": "object",
        "required": ["fileName", "contentType", "base64"],
        "properties": {
          "type": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DocumentType"
              }
            ],
            "description": "O tipo do arquivo. **Opcional**: sem ele, o agente diz qual é — e aí `agentReader` é obrigatório."
          },
          "fileName": {
            "type": "string",
            "description": "O nome do arquivo.",
            "example": "bo.pdf"
          },
          "contentType": {
            "type": "string",
            "description": "O formato do arquivo.",
            "enum": [
              "application/pdf",
              "image/jpeg",
              "image/png",
              "image/webp"
            ],
            "example": "application/pdf"
          },
          "base64": {
            "type": "string",
            "description": "O arquivo em Base64 padrão. Até 10 MB decodificado.",
            "example": "JVBERi0xLjQKJeLjz9MK..."
          },
          "metadata": {
            "type": "object",
            "description": "Objeto JSON livre, guardado no documento. As leituras dos agentes entram aqui, cada uma na chave do seu agente.",
            "additionalProperties": true
          },
          "agentReader": {
            "type": "boolean",
            "description": "Manda o OCR do tipo ler o arquivo e gravar os campos no `metadata`. Obrigatório quando não há `type`.",
            "default": false,
            "example": true
          },
          "autoCreate": {
            "type": "boolean",
            "description": "Manda a leitura deste arquivo entrar na montagem do caso: os envolvidos e o relato saem dela. Exige `agentReader`.",
            "default": false,
            "example": true
          }
        }
      },
      "Document": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "researchId": {
            "type": "string",
            "format": "uuid"
          },
          "researchEntityId": {
            "type": "string",
            "description": "O envolvido a que o documento pertence. Vazio quando é do caso.",
            "example": ""
          },
          "type": {
            "$ref": "#/components/schemas/DocumentType"
          },
          "fileName": {
            "type": "string",
            "example": "bo.pdf"
          },
          "contentType": {
            "type": "string",
            "example": "application/pdf"
          },
          "base64": {
            "type": "string",
            "description": "O arquivo. Vem vazio na listagem com `?content=false`."
          },
          "metadata": {
            "type": "object",
            "description": "O que veio no envio, mais o que cada agente gravou na chave dele: `identifyDocument` (o que o arquivo é), `ocrBo`, `ocrApolice`, `ocrAvisoSinistro`, `ocrTermoTerceiro`, `ocrCnh`, `ocrCrlv`, `ocrVehicle` (as leituras) e `autoCreate` (o arquivo entrou na montagem do caso).",
            "additionalProperties": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "UpdateDocumentRequest": {
        "type": "object",
        "required": ["type", "fileName", "contentType", "base64"],
        "description": "Substitui o documento inteiro. O que não vier é apagado.",
        "properties": {
          "researchEntityId": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "$ref": "#/components/schemas/DocumentType"
          },
          "fileName": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "base64": {
            "type": "string"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "CreateEntityRequest": {
        "type": "object",
        "required": ["type", "roles"],
        "properties": {
          "type": {
            "$ref": "#/components/schemas/EntityType"
          },
          "value": {
            "type": "string",
            "description": "A placa (obrigatória no `VEHICLE`) ou o CPF/CNPJ, com ou sem máscara. Em `PERSON` e `COMPANY` é opcional — sem ele, não há consulta de cadastro.",
            "example": "TST1A23"
          },
          "roles": {
            "type": "array",
            "description": "Os papéis do envolvido no caso. Pelo menos um. A mesma pessoa pode ter vários: o segurado que dirigia é `SEGURADO` e `MOTORISTA`.",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/EntityRole"
            }
          },
          "parentId": {
            "type": "string",
            "format": "uuid",
            "description": "O veículo a que este envolvido se liga. O `VEHICLE` é sempre raiz; a `COMPANY` só fica sob um `VEHICLE`."
          },
          "reason": {
            "type": "string",
            "maxLength": 2000,
            "description": "Por que este envolvido está no caso, em texto livre."
          },
          "profile": {
            "type": "object",
            "description": "Cadastro livre. A consulta paga entra numa chave dele (`dataset` ou `asset`), sem apagar o que você mandou.",
            "additionalProperties": true
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "externalId": {
            "type": "string",
            "description": "Ignorado: o envolvido nasce sem id externo.",
            "deprecated": true
          }
        }
      },
      "UpdateEntityRequest": {
        "type": "object",
        "description": "Um PATCH. `type`, `externalId` e `manual` são ignorados; `value` é imutável.",
        "properties": {
          "roles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntityRole"
            }
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "`null` não mexe; `\"\"` apaga."
          },
          "parentId": {
            "type": "string",
            "nullable": true,
            "description": "`null` não mexe; `\"\"` desliga o vínculo."
          },
          "profile": {
            "type": "object",
            "additionalProperties": true
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "value": {
            "type": "string",
            "description": "Se vier, tem de ser exatamente o valor gravado. Qualquer outro é `400 IDENTITY_IS_IMMUTABLE`."
          }
        }
      },
      "Entity": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "researchId": {
            "type": "string",
            "format": "uuid"
          },
          "parentId": {
            "type": "string",
            "description": "O veículo a que este envolvido se liga. Vazio quando é raiz.",
            "example": ""
          },
          "externalId": {
            "type": "string",
            "example": ""
          },
          "type": {
            "$ref": "#/components/schemas/EntityType"
          },
          "value": {
            "type": "string",
            "example": "TST1A23"
          },
          "roles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EntityRole"
            }
          },
          "reason": {
            "type": "string"
          },
          "profile": {
            "type": "object",
            "description": "O cadastro: o que você mandou, mais a consulta na chave `dataset` (Zarv ID v3) ou `asset` (Zarv Asset).",
            "additionalProperties": true
          },
          "manual": {
            "type": "boolean",
            "description": "`true`: declarado por quem abriu o caso. `false`: achado da apuração. A diferença é de peso probatório.",
            "example": true
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "KanbanLane": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "workspaceId": {
            "type": "string",
            "format": "uuid"
          },
          "userId": {
            "type": "string",
            "description": "Quem criou a raia. `system` nas nove do quadro padrão.",
            "example": "system"
          },
          "phase": {
            "type": "string",
            "description": "A fase da raia. Uma raia nunca troca de fase.",
            "enum": ["COLETA", "ANALISE", "DESFECHO"],
            "example": "COLETA"
          },
          "step": {
            "type": "string",
            "description": "O nome da raia.",
            "example": "Abertura"
          },
          "description": {
            "type": "string",
            "example": ""
          },
          "order": {
            "type": "integer",
            "description": "A posição dentro da fase. Pode repetir; o empate é desfeito pela criação.",
            "example": 1
          },
          "status": {
            "type": "string",
            "description": "`INACTIVE` esconde a raia sem apagá-la — ela continua na lista, e quem a esconde é o cliente.",
            "enum": ["ACTIVE", "INACTIVE"],
            "example": "ACTIVE"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}
