Quando um pipeline com LLM escreve conteúdo direto no seu repositório, a pergunta de engenharia não é “e se a IA errar?” — ela vai errar. A pergunta certa é: onde o erro é permitido existir? A resposta deste blog: o erro pode existir no rascunho, no Pull Request, no preview — em qualquer lugar antes da produção. Nunca depois. Isso não se garante com prompt bem escrito; garante-se com contrato executável.

Este post descreve o contrato de conteúdo do Fluxonar — um schema Zod que valida o frontmatter de cada post no build — e o caso real desta semana em que ele barrou um erro meu, humano, exatamente como barraria o do LLM.

O contrato é uma API, não uma convenção

O pipeline editorial daqui (descrito em outro post) termina com o N8N escrevendo um arquivo markdown e abrindo um PR. Entre o que o LLM escreve e o que o Astro aceita existe um schema declarado em código:

  • title: 10 a 70 caracteres — o limite prático de exibição em resultado de busca.
  • description: 50 a 160 caracteres — a janela da meta description.
  • category: enum fechado (tech, ia, automacao). Não existe “categoria nova” por acidente.
  • tags: no máximo 6, e cada uma precisa existir num arquivo versionado (tags.json). A lista é a fonte única; texto livre não entra.
  • pubDate: data ISO validada; draft: booleano com default seguro.

Markdown que viole qualquer regra falha o build. E como o deploy do preview roda o build, o PR quebrado nunca gera preview aprovável — o erro morre na esteira, visível, com mensagem apontando o campo. Fail-closed: o caminho do erro até a produção não existe por construção.

Prompt não é garantia. Contrato executável é: o build recusa o que o modelo inventar fora do combinado.

O caso real: a tag que não existia

Escrevendo o post anterior — eu, humano, com revisão — usei três tags: cloudflare, dns, seo. O build falhou na hora: dns não existe em tags.json. Não é bug; é o desenho funcionando. A taxonomia é deliberadamente fechada: tag nova exige mexer num arquivo versionado e protegido, ou seja, exige uma decisão humana explícita — nunca nasce de improviso no meio de um post.

Por que isso importa mais ainda com LLM no circuito: modelos adoram inventar taxonomia. Peça dez posts e você ganha cloud, cloudflare, cdn, hospedagem — quatro etiquetas para a mesma coisa, e um arquivo de tags que vira aterro sanitário. Com enum fechado, o modelo pode sugerir o que quiser; o build só aceita o vocabulário decidido por gente.

Por que validar duas vezes

Um detalhe do desenho que parece redundância e não é: a validação existe em dois pontos. O nó de validação do N8N confere o frontmatter antes de escrever o arquivo (feedback rápido, ainda dentro do pipeline, onde dá para re-gerar barato). O schema do Astro confere de novo no build (garantia dura, independente de quem escreveu — pipeline, humano, script).

A primeira validação é conveniência; a segunda é segurança. Se elas divergem, a conveniência mente — por isso as duas rodam a mesma lógica de validação, extraída para um módulo único, com teste automatizado provando a paridade. Regra geral para qualquer pipeline: a validação de dentro pode ser cópia da de fora, nunca uma interpretação dela.

O princípio portável

Nada disso é específico de blog ou de Astro. A receita, portável para qualquer sistema onde uma IA produz artefatos:

  1. Declare o contrato em código executável (schema, tipos, enum) — não em documento de boas intenções que ninguém executa.
  2. Feche os vocabulários. Tudo que for classificação (categoria, tag, status, tipo) é enum versionado, nunca texto livre do modelo.
  3. Acople a validação ao caminho da publicação. Se dá para publicar sem passar pelo contrato, o contrato é decorativo.
  4. Deixe o erro ser barato. Falha no build de um PR custa um clique de re-gerar; falha em produção custa credibilidade — e no caso de conteúdo, ranqueamento.

O gate humano decide se o texto merece ir ao ar. O contrato garante que, tecnicamente, só chega à decisão dele o que já está estruturalmente correto. São camadas diferentes — e é a soma das duas que deixa um pipeline de IA operar sem medo.