Como fundir duas bases de conhecimento sem perder nenhuma regra

28 de setembro de 2026 · 14 min de leitura

Quando surge uma dúvida sobre regra fiscal, por exemplo que regra se aplica a um campo novo da NF-e, ou desde quando o IBS entra na nota, a resposta está em algum documento oficial. Pode ser uma Nota Técnica da SEFAZ, um ato do comitê gestor do IBS ou a lei complementar da Reforma Tributária. São mais de cem documentos desse tipo, e responder uma pergunta dessas era abrir PDF por PDF até achar o trecho certo.

Hoje eu faço a pergunta a um agente de IA. Antes de responder, ele consulta uma base onde esses documentos já estão processados e indexados, e devolve a regra na hora, dizendo de qual documento ela saiu.

O agente confia na base. Se ela está errada, ele responde errado com a mesma segurança com que responde certo. Se falta uma regra, ele diz que a regra não existe. A base só é útil enquanto cada documento dentro dela está no lugar certo.

Um exemplo do que sai do lugar sem ninguém ver: a numeração. Nota Técnica é o documento que a SEFAZ publica pra mudar as regras da nota fiscal eletrônica: campo novo, validação nova, data em que a mudança passa a valer. Cada uma tem um número, tipo 2025.001, e eu tratava esse número como a identidade do documento. Só que a SEFAZ mantém séries de numeração paralelas, e o 2025.001 hoje serve três documentos diferentes na base: a nota ordinária, uma de CT-e ligada à Reforma Tributária e uma nota conjunta. O 2024.002 serve dois. Uma ferramenta que procura documento pelo número acha um dos três e segue em frente sem avisar. Foi assim, escolhendo em silêncio, que a mesma Nota Técnica acabou existindo em três lugares da base, com três extrações diferentes.

Esse texto é sobre o trabalho de juntar duas bases que tinham divergido desse jeito, sem perder nenhuma regra no caminho. O assunto é fiscal, mas o método vale pra qualquer base de conhecimento que um humano mantém e um agente de IA consulta.

O que é a base

É uma pasta de arquivos de texto com as fontes fiscais oficiais já processadas: resumo de cada documento, regras de validação em JSON, os XSD (os arquivos que definem o formato do XML da nota), o cronograma de transição da Reforma Tributária do Consumo até 2033. Medido em setembro de 2026: 186 documentos indexados, 2.380 regras de validação, 1.643 tags de XML mapeadas e 201 XSD versionados.

Quem consulta é o Claude Code, um agente de IA que executa tarefas no computador em vez de só responder no chat. Antes de responder qualquer pergunta sobre NF-e ou sobre IBS e CBS, ele roda um script de busca que devolve só o trecho relevante. É o que se chama de RAG (buscar o texto certo e entregar ao modelo junto com a pergunta), numa versão bem simples: busca textual com grep e jq, sem banco vetorial e sem embeddings.

Com essa arquitetura, quando a base está errada o agente responde errado com toda a convicção. E quando falta uma regra, ele responde que a regra não existe.

Duas pastas não fazem dois projetos

A base começou em duas. Uma cuidava das Notas Técnicas da SEFAZ, a outra da Reforma Tributária e de uma camada derivada interna, com guias e resumos escritos por nós a partir das normas. Foram criadas com 11 dias de diferença, no mesmo repositório, e atualizadas nos mesmos commits: 6 dos 10 últimos commits de cada lado eram o mesmo hash. Não havia versionamento, release, permissão nem CI separados: só o nome da pasta distinguia uma da outra.

Mesmo assim divergiram, em quatro frentes:

  1. Dado. Uma regra de devolução da NT 2025.002-RTC estava com vigência “2025-10” nas duas bases, errada nas duas.
  2. Código. O script de busca teve cerca de 65% reescrito de forma independente em cada lado. Só um lado escapava caractere especial de expressão regular; só o outro ordenava por relevância. O lado com a base maior rodou dois meses sem a correção de desempenho que o outro já tinha.
  3. Trabalho fiscal. A mesma NT 2025.002 existia em três unidades, com versões e profundidade de extração diferentes.
  4. Engenharia gasta só em vigiar a duplicação: 2 especificações, 2 subagentes e 1 skill existiam pra isso.

Ninguém percebeu porque nenhuma dessas quatro coisas dá erro. O único sinal registrado foi uma observação de que um lado estava 15 dias sem atualização e o outro, 1 dia. Li aquilo como problema de cadência. A causa era estrutural: as duas pastas não tinham dono nem ciclo próprios, e a cadência diferente vinha daí.

O teste que uso hoje pra saber se dois projetos são dois projetos: existe algum commit que toca um e não o outro por um motivo? Se não existe, é um projeto só com um if no lugar errado.

O campo que respondia duas perguntas

A regra de devolução com a vigência errada mostra de onde vem esse tipo de divergência.

A data “2025-10” veio de uma tabela da própria Nota Técnica, só que da linha errada. A tabela tem uma linha pra implantação do schema e outra pra aplicação das regras de validação, com datas diferentes. Alguém pegou a data plausível da linha vizinha. A vigência real estava na versão 1.40 da NT, com produção em 01/09/2026. O erro virou incidente rastreado.

Por trás da data havia um problema no desenho do dado: cada regra tinha um campo de confiança só. As 240 regras daquele guia estavam todas marcadas com o nível mais alto, base legal, e 172 delas tinham data não verificada. O campo misturava duas perguntas, “quanta autoridade tem o texto da regra?” e “de onde veio a data?”. Com um campo só, não havia como escrever “a regra está certa, mas a data foi herdada de outro lugar”.

A correção foi separar em dois campos. Com isso, “base legal com data herdada” virou um estado que se pode gravar e procurar, e na fusão isso permitiu achar e corrigir 166 regras de uma vez. A regra que ficou pra mim: quando um campo de qualidade tem o mesmo valor em todas as linhas, ele provavelmente está respondendo mais de uma pergunta.

Como fundir sem perder regra

A fusão foi feita em agosto de 2026, com a ajuda do próprio Claude Code. O que segue é o que funcionou, na ordem de importância.

Conferir item a item com um ledger

Na época a base tinha 2.233 regras. Comparar “2.233 antes, 2.233 depois” não prova nada: os totais podem empatar por acidente, com uma regra perdida e outra duplicada. A verificação que usei foi um ledger, um livro-razão: pra cada par (documento de origem, código da regra) do estado anterior, ou o código existe no destino, ou existe uma linha no ledger explicando pra onde foi ou por que saiu. Um par sem explicação quebra o build. O ledger ficou versionado junto da base e hoje responde perguntas como “por que essa regra tem essa vigência”.

Congelar o baseline fora da pasta que muda

A primeira versão do verificador comparava a base com ela mesma e passava sempre, porque as pastas de origem já tinham perdido 141 unidades numa etapa anterior. Pra piorar, a cópia de segurança não tinha o arquivo de baseline: ela foi feita antes do censo rodar. Congelar o estado inicial e conferir que ele de fato ficou congelado são dois passos diferentes.

Escolher o vencedor campo a campo

Nenhum lado era melhor em tudo. O lado com a extração mais rica estava numa versão antiga da norma; o lado atualizado tinha 5 regras que só ele tinha e que sumiriam se eu descartasse “o lado pior”. Na hora de fundir, listei o que cada um tinha e o outro não, campo a campo. O instinto de escolher o melhor lado perde justamente as correções que só o lado pior recebeu.

Mapear campo pelo conteúdo

Os dois lados usavam nomes diferentes pros mesmos campos, e o mapeamento parecia óbvio. Três de quatro pares estavam errados. O campo chamado msg não guardava mensagem: guardava o código numérico de rejeição da SEFAZ. A mensagem estava em descricao_erro. Um mapeamento feito pelo nome teria gravado código de rejeição no lugar da mensagem em 471 regras.

Medir antes de renomear em cascata

Um campo chamado id_nt parecia ser o identificador da Nota Técnica. Os valores eram LA17, N02a, W06b.1: identificadores de campo do XML. Se eu tivesse incluído esse campo na renomeação em lote, teria corrompido o dado.

Chave composta quando a fonte reusa identificador

A própria norma reusa o mesmo código de regra com rejeições diferentes. A chave de uma regra passou a ser o par (código da regra, código de rejeição), não o código sozinho.

Deixar maiúscula e minúscula como estão

Achei 4 pares de tags que só diferiam na caixa. Dois eram homógrafos legítimos, de leiautes diferentes: vCOFINS na NF-e e vCofins na NFS-e. Um lower() teria fundido duas tags distintas. Cada colisão passou por uma escada de arbitragem (primeiro o XSD, depois o PDF da norma, por último uma pessoa), e o degrau que resolveu cada par ficou registrado.

Deixar o valor desconhecido existir

A tabela de cronograma de um dos PDFs usa células mescladas que atravessam quebra de página. A extração de texto achata isso, e onde vários códigos se empilham numa célula já não dá pra saber qual data é de qual código. Inventar uma data seria fabricar dado. Criei um valor novo que o plano não previa, “citada sem data”, que diz exatamente o que se sabe.

Desconfiar do próprio conserto

Ao corrigir códigos que um dos lados tinha fabricado, eu mesmo criei uma regra fantasma: um código ganhou 3 variantes porque o valor fabricado não saiu quando o PDF tinha duas leituras válidas. O verificador pegou o erro; a revisão tinha deixado passar.

Quando o número casa com mais de um documento

Volto à numeração do começo. Depois da fusão, o script que confere se uma Nota Técnica do portal já está na base poderia usar um mapa simples de número pra documento. Seria mais fácil de escrever e mais fácil de ler. Só que um mapa 1:1 faz a ferramenta responder “já indexado” apontando pro documento errado. Foi esse tipo de silêncio que criou as três cópias da NT 2025.002.

Então, quando o número casa com mais de um documento, o script responde “ambíguo”, mostra a lista de candidatos e deixa a decisão pra uma pessoa. O mesmo princípio vale pro resto: regra não localizada no PDF, regra citada sem data, código sintético descartado. Cada um tem sua linha no ledger, e nada sai da base sem registro.

O defeito que não dá erro

Numa base consultada por agente, a falha típica não levanta exceção. O script termina com sucesso, imprime “nenhum documento encontrado”, e quem lê conclui que a regra não existe. Alguns casos reais:

  • O index() do jq devolve a posição do item encontrado, e a posição do primeiro item é 0, que o jq trata como falso. O filtro descartava sempre o primeiro resultado. Estava assim desde a criação do script.
  • Um problema de aspas com o caractere de tabulação desligou a busca por conteúdo inteira de um dos lados, e isso durou 4 especificações.
  • O jq no Windows emite fim de linha com \r. Num hook novo, escrito durante a própria fusão, isso fez todo arquivo de resumo parecer inexistente.
  • Um arquivo derivado, o índice de regras por campo do XML, ficou um mês sem ser regenerado. Quando olhei, 229 das 578 tags não tinham entrada e 55 documentos estavam fora dele. Os 22 critérios de teste da base passavam o tempo todo, porque nenhum olhava aquela pasta.

O do index() e um outro, de conversão de caminho de arquivo entre o Git Bash e o jq do Windows, só apareceram na fusão. Os bugs já estavam lá, e nenhum dos lados tinha o caso de teste que os mostraria.

Daí a regra de teste que a base adotou: conferir a presença do resultado esperado, nunca “a saída não está vazia”. Saída não vazia passa com o resultado errado. E quando o script corta resultados por limite, ele escreve no rodapé quantos ficaram de fora. Sem isso, resultado cortado e resultado inexistente parecem a mesma coisa, e o agente responde “não há regra sobre isso” com convicção.

O caso de nome é parecido. Uma varredura achou o mesmo conceito gravado com nomes diferentes: em 451 regras, o campo do XML estava numa chave chamada tag em vez de campo. Tudo que dependia de campo pulava essas 451 regras sem avisar. O que denunciou foi que as duas chaves nunca apareciam juntas na mesma regra.

A tarefa agendada que atualiza a base teve o mesmo tipo de falha. Uma mudança no script da tarefa agendada, feita pra resolver outro problema, passou a quebrar o prompt em pedaços. O agente recebia só a primeira palavra, “Execute”, respondia perguntando o que era pra executar, e terminava com código de saída 0. O painel mostrava sucesso e a skill não rodava. Código de saída 0 só diz que o agente terminou. Em automação sem ninguém olhando, o que mostra trabalho feito é a evidência dele: o relatório no log, ou o arquivo alterado.

A decisão que a medição desfez

O plano da fusão previa migrar os dois scripts que baixam novidades do portal da SEFAZ e do comitê gestor do IBS pra Playwright, uma ferramenta que controla um navegador de verdade. A justificativa era plausível: o portal é ASP.NET, cheio de postback, e ler página estática seria frágil.

Antes de migrar, conferi a justificativa. Três coisas derrubaram o plano. A listagem de Notas Técnicas do portal vem pronta num único GET, sem postback: 101 NTs com uma requisição simples em Python. O lado do comitê gestor só lê o sitemap.xml, que é XML puro. E o Playwright estava configurado como servidor MCP (o protocolo que liga um agente de IA a ferramentas externas), e um script Python não fala MCP; quem usa MCP é o agente. Migrar o script significaria embutir a biblioteca e uns 400 MB de navegador.

A migração não foi feita. No lugar dela saíram um detector único e um ingestor único, cada um substituindo dois scripts que eram o mesmo programa duplicado, cada cópia com metade das capacidades. O Playwright ficou pro que precisa de navegador de fato, usado pelo agente.

A razão declarada de uma decisão técnica e a razão real divergem com frequência. Testar a razão declarada é mais barato que executar a decisão.

O que ficou

Das regras que tirei desse trabalho, estas são as que valem fora do domínio fiscal:

  1. Pra provar que nada se perdeu, use ledger item a item, não total. Todo item que sai tem uma linha explicando pra onde foi.
  2. Congele o estado inicial fora da árvore que vai mudar, e confira que congelou.
  3. Mapeie campo pelo conteúdo medido, nunca pelo nome.
  4. Quando a fonte é ambígua, a ferramenta reporta a ambiguidade e para. Mapa 1:1 onde a realidade não é 1:1 só esconde o problema.
  5. Teste a presença do resultado esperado. Saída não vazia e código de saída 0 não provam nada.
  6. Regra sem verificador é regra que se descumpre sozinha. A regra “índice agregado guarda só ponteiro pra regra, nunca a regra inteira” estava escrita desde a fusão, e mesmo assim o índice por evento carregava 56 regras inteiras até um verificador começar a olhar.

Documentação e índice derivado são cópias de outra coisa, e cópia diverge em silêncio quando ninguém confere. Numa base consultada por agente, o sintoma é uma resposta que some sem nada quebrar.

discuta no LinkedIn