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.ymle nodocs.yml. Noci.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 é ocheca-nav.py, que roda no gate de código pela mesma razão. Nodocs.yml, para pegar o caminho inverso: prosa editada sem tocar código, inclusive pela interface web do Forgejo, que não dispara oci.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.yamljá 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õePUTeDELETE— 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.