Pular para conteúdo

0012 — O contrato REST é checado contra o OpenAPI gerado

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-16 · Decidido em: 2026-07-16

Contexto

A constituição §6 manda a doc mudar no mesmo PR que muda o contrato, e a própria §6 admitia, em letras miúdas, que isso era "disciplina de fechamento + o gate do ci.yml, não um detector automático de 'doc seguiu código'".

O custo dessa lacuna apareceu num app: uma rota de upload foi renomeada no controller. O OpenAPI — que o micronaut-openapi deriva dos @Controller/@Post no momento da compilação — acompanhou sozinho. A prosa não. Por 10 dias, mkdocs build --strict, checa-nav.py e o ci.yml ficaram todos verdes, enquanto o site publicado servia, no mesmo domínio, o contrato novo (Swagger UI) e a prosa com a rota morta (decisões). Nenhum gate mentiu: nenhum deles jamais afirmou nada sobre a relação entre os dois.

A classe é mais ampla que o caso. Sempre que a prosa afirma algo que um artefato gerado também afirma, existe um oráculo de graça — e não usá-lo é escolher que o drift só apareça quando um leitor tropeçar nele.

Decisão

Onde a afirmação da prosa tem um artefato gerado para conferir contra, o gate confere. Hoje isso é rota REST × OpenAPI, via scripts/checa-rotas.py (rastreado, stdlib, baixado fresco do site, tolerante).

  • Direção: prosa → spec. O spec sai do código, logo é a verdade; a prosa é a afirmação a conferir. O caminho contrário — cobrar que toda rota do spec apareça na prosa — seria cobrança de cobertura de doc: ruidoso (33 paths num app real) e contra a §1.2, que manda não documentar o inferível.
  • Roda no ci.yml e no docs.yml. No ci.yml, depois do ./gradlew check (é quando o spec existe), porque o rename acontece no PR de código e é lá que tem que doer — o precedente é o checa-nav.py, que roda no gate de código pela mesma razão. No docs.yml, para pegar o caminho inverso: prosa editada sem tocar código, inclusive pela interface web do Forgejo, que não dispara o ci.yml.
  • Escopo por heurística, não por allowlist. Só é cobrada a rota cujo primeiro segmento existe no spec. Prosa cita legitimamente rota que não é deste app — API de parceiro que o app só consome (POST /sync/erp), rota de app vizinho documentado no mesmo repo (POST /debug/...) — e uma allowlist manual seria mais uma lista que envelhece, isto é, o drift que estamos matando.
  • Escapes graduados: rota que é prefixo por segmento de uma do spec passa (diagrama de topologia nomeia a família); documento status: obsoleto é pulado inteiro (já se declara não-atual); e a linha com <!-- checa-rotas: ignorar --> é pulada — escape explícito, escopo de linha, para a decisão de remoção, que não tem como dizer "removemos X" sem escrever X.

Na mesma leva, o piso §6 passa a nomear o teste que reimplementa a lógica que atesta como falso atestado, irmão da fixture inventada que ele já citava; e o ci-testes registra o teste de mutação como ferramenta de diagnóstico sob demanda, não gate.

Alternativas consideradas

  • Não fazer o check, só reforçar a norma. Rejeitada: a norma já existia e era exatamente o que falhou. Repetir a regra mais alto não é mecanismo.
  • Checagem bidirecional. Rejeitada — vira cobrança de cobertura de doc (acima).
  • Allowlist declarada em vez da heurística do primeiro segmento. Rejeitada — lista que envelhece. Custo aceito: renomear o primeiro segmento inteiro (/api/*/v2/*) faz a prosa velha virar "rota de terceiro" e passar. Ponto cego declarado no script.
  • Só no docs.yml. Rejeitada: é o workflow que não roda no PR de código, e o rename é um evento de código. Foi assim que o drift viveu 10 dias.
  • Ler o docs/public/api/openapi.yaml já publicado, em vez do gerado. Rejeitada: num app real esse arquivo é um spec escrito à mão que descreve a API do parceiro, não a do app. O default lê só o artefato gerado; o caminho por argumento fica para quem sabe o que está apontando.
  • Teste de mutação como gate. Rejeitada: minutos por build e incentivo a gaming — mesma razão de "cobertura é visível, não bloqueante".

Consequências

  • Todo app Java que gera OpenAPI ganha o check ao re-puxar o ci.yml. App headless, biblioteca e Flutter não têm spec → o check é no-op, como os templates rastreados opcionais.
  • O check só sabe de rota e método — corpo de request/response segue coberto por revisão de PR.
  • Aplicar o check aos dois apps que geram OpenAPI hoje achou, além do drift que motivou a decisão, um segundo bug que ninguém tinha visto: um doc cuja seção diz "Envie um PUT" e cujo exemplo diz POST /api/v1/xadm, numa rota que só expõe PUT e DELETE — a prosa se contradizendo na mesma frase. Correção é migração oportunista nos repos dos apps, e a mesma passada exige um <!-- checa-rotas: ignorar --> na decisão de remoção que nomeia a rota removida.