Toda costura escrita à mão precisa de um guard. Crossweft, uma verificação de contrato entre linguagens para agentes de IA
Autora: Anastasiia Butova, engenheira de ComfyUI e modelos de difusão, Belgrado · publicado em 2026-10-08
Tradução da versão em inglês: English
Nossos bugs mais caros nunca estiveram dentro de um arquivo. Estiveram entre arquivos. Um valor era escrito em dois ou três lugares, em duas linguagens, alguém mudava um deles, tudo continuava compilando, os testes ficavam verdes, e a instalação quebrava na máquina de um cliente. Este é o jeito que encontramos para tornar esses lugares explícitos e verificáveis, e para embutir a verificação nos agentes de IA que escrevem uma parte grande do nosso código. A ferramenta é open source: github.com/AnastasiyaW/crossweft, página do projeto: happyin.work/crossweft.
Dez fronteiras que o compilador não enxerga
Construímos um plugin de retoque para Photoshop que roda uma rede neural na máquina do próprio usuário. Entre o fotógrafo e o modelo há dez fronteiras de confiança: um canal protegido entre o plugin e um serviço do sistema, registro de dispositivo, artefatos assinados com carimbos de tempo confiáveis, atestação de build, o canal de atualização, criptografia do modelo e uma marca d'água. As partes são escritas em C++ e Go, rodam em processos diferentes e chegam por instaladores diferentes.
Nenhuma dessas fronteiras é um módulo. Cada uma é um acordo entre dois lados: o mesmo nome de serviço, a mesma versão de protocolo, o mesmo conjunto de rotas, a mesma string de confirmação. Hoje, 54 elos atravessando essas fronteiras carregam 188 comparações de valores (468 lugares no código) e 105 comparações de conjuntos. O compilador não vê nenhuma delas, porque os dois lados estão em linguagens diferentes.
Começamos a chamar esses lugares de costuras (seams): pontos em que dois componentes precisam concordar exatamente.
- a rota que um cliente chama e a rota que o servidor registra;
- o header com a versão do protocolo;
- os campos de um DTO em Go e em TypeScript;
- as variáveis de ambiente que um serviço lê e as listadas no
.env.example; - uma regra de arredondamento de preço escrita duas vezes, em duas linguagens.
Tipos, testes e linters funcionam bem dentro de uma linguagem. Raramente verificam acordos como esses, então eles ficam guardados na memória das pessoas.
O nome vem da trama (weft), o fio que corre transversal à urdidura e mantém o tecido inteiro.
Três histórias
Um identificador, três cópias. O nome do serviço era resolvido em três funções quase idênticas. Numa máquina limpa, o serviço ainda não estava instalado, a busca falhava, uma máquina vazia parecia uma instalação antiga incompatível, e o instalador parava com código de saída 76. Corrigimos uma cópia. As outras duas continuaram quebradas.
Duas metades de uma mesma dependência. O build pegava uma biblioteca de um diretório de instalação e suas ferramentas de outro. A configuração só imprimia um aviso, então o problema aparecia muito depois, onde a causa real era difícil de achar.
Um guard que não guardava nada. Um validador de uma das nossas regras de segurança tinha um caminho errado. Durante semanas, ele verificou zero arquivos e reportou OK todas as vezes. Passava justamente porque não rodava.
A causa é a mesma nas três: um acordo tem dois ou três lados, e se eles batem depende de alguém prestar atenção.
Por que agentes de IA pioram isso, e como também ajudam
Uma parte grande do nosso código é escrita por agentes, principalmente Claude Code e Codex. Eles lidam com acordos entre componentes pior do que as pessoas. O caso típico: o agente muda uma struct do Go, roda os testes, vê verde e diz "pronto", sem nunca abrir a interface TypeScript do outro lado da costura.
- Edições locais. O outro lado da costura está em outra pasta, outra linguagem, outro processo.
- Contexto limitado. O agente não tem o produto inteiro na cabeça, e o compilador não lhe diz que um cliente em algum lugar depende daquela struct.
- Uma ideia estreita de "pronto". Para um agente, pronto significa "todas as verificações que eu consigo ver passaram". Se nenhuma verificação olha a costura, o desvio fica invisível até que algo quebre.
Agentes seguem bem instruções locais e precisas. Diga a um agente, logo depois da edição, qual arquivo do outro lado ele deve abrir e o que precisa bater ali, e ele faz. Não o deixe terminar enquanto os dois lados discordam, e o erro nunca chega a um commit.
A regra: todo elo diz como se mantém em acordo
Todo elo entre componentes declara como seus dois lados se mantêm em acordo. Se os dois lados são escritos à mão, o elo precisa de um guard: uma verificação que lê os dois lados.
Os elos vivem em arquivos JSON ao lado do código. O mapa tem blocos (programas, serviços, armazenamentos, arquivos de configuração) e elos entre eles (HTTPS, um named pipe, arquivos, geração de código, variáveis de ambiente). Toda afirmação do mapa é ancorada a um literal num arquivo-fonte, então quando o código se move, o mapa não consegue continuar descrevendo o passado em silêncio. A verificação falha.
Cada elo tem um campo contract.enforcement:
| Enforcement | Quem mantém os lados iguais | O que a verificação exige |
|---|---|---|
shared-code |
um símbolo que os dois lados importam | defined_in nomeia o símbolo |
generated |
um gerador a partir de uma única fonte | defined_in nomeia a fonte |
schema-tests |
vetores de teste compartilhados | defined_in nomeia os vetores |
duplicated |
ninguém; é copiado à mão | um guard que lê um arquivo de cada lado |
convention |
ninguém; é combinado | um guard que lê um arquivo de cada lado |
none |
ninguém | um guard que lê um arquivo de cada lado |
A classificação sozinha já é útil. "Onde ainda dependemos da memória de alguém?" vira uma lista que dá para contar. Um guard que lê só um lado não conta: a ferramenta verifica que ele lê arquivos em duas pontas diferentes do elo.
Três guards: join, set e pair
Os guards são deliberadamente simples: expressões regulares, comparação de valores ou de conjuntos de valores. Uma verificação leva milissegundos, não precisa de build e funciona com qualquer linguagem.
join: o valor é o mesmo em todo lugar. A versão da API que o cliente TypeScript envia e a que o servidor Go espera:
{"id": "api-version", "link": "web-orders", "points": [
{"path": "web/src/api.ts", "side": "web", "regex": "API_VERSION = \"([^\"]+)\""},
{"path": "server/main.go", "side": "api", "regex": "APIVersion = \"([^\"]+)\""}]}
Toda ocorrência é comparada, não apenas a primeira. Nossa primeira versão checava só a primeira correspondência, então uma segunda cópia mais abaixo no arquivo podia passar despercebida.
set: os membros batem. Campos de uma interface TypeScript contra as tags JSON do Go; os status que um worker Python escreve contra as constantes do Go; as variáveis de ambiente que o código lê contra o .env.example. Uma diferença intencional vai para allow com um motivo, e uma entrada de allow que já não é necessária faz a própria verificação falhar.
pair: lógica duplicada sem um único valor para comparar. Por exemplo, arredondamento de preço em Go e em TypeScript. As duas regiões são marcadas com crossweft:begin price-rounding e crossweft:end price-rounding, e o Crossweft calcula uma impressão digital delas. Se qualquer uma mudar, a verificação falha até que alguém releia o gêmeo e rode crossweft attest price-rounding --reason "...". O motivo vai para o arquivo de lock e para a revisão.
A saída mais curta de um pair vermelho para um agente é atestar o lado que ele mudou e nunca tocar no outro. Por isso, se só uma região mudou desde a última atestação, o attest se recusa. Corrija o gêmeo primeiro ou, se ele realmente já está equivalente, diga isso explicitamente com --other-side-unchanged.
Há verificações maiores também. Toda rota que o servidor registra precisa estar no mapa; o Go chi é lido nativamente (também Express e Fastify, FastAPI, Flask, gin e echo), e o resto por meio de uma expressão regular. Toda chamada /v1/... no código do cliente precisa ser explicada por algum elo. Um bloco só pode enviar dados que criou ou recebeu. Toda pasta de código-fonte precisa estar no mapa.
Quando os tipos embutidos não bastam, um repositório pode adicionar os seus como plug-ins. Temos 13. Um compara 916 lugares em que o servidor Go conversa com SQL com 161 tabelas e 276 triggers do schema; outro verifica 185 rotas do servidor; um terceiro verifica 27 pares "waiter ≥ work + margin" de timeouts. Um plug-in que falha ao importar, quebra ou não compara nada é um erro, nunca um resultado verde.
O que o agente vê
A verificação é metade da história. O agente precisa ficar sabendo de uma costura logo depois da edição, e não quando o código está pronto para commit. Três hooks fazem isso:
- Início da sessão: uma nota curta de que o repositório tem um mapa de costuras, quantas costuras existem e como trabalhar com elas.
- Depois de cada edição: se o arquivo está numa costura, o agente é informado do outro lado e do guard.
- Antes de terminar:
crossweft checkroda. Se uma costura está em desacordo, o agente não consegue terminar e é informado do que fazer.
Depois de uma edição no servidor Go na demo, o agente vê:
crossweft: server/main.go is part of 3 cross-component seam(s). Keep both sides in agreement:
- link web-orders (web -> api, https; Orders API [duplicated]): you changed its to side.
Re-read: web/src/api.ts. Guarded by: join:api-version, join:version-header,
set:order-fields, set:web-statuses, pair:price-rounding.
...
Run `crossweft check` before you finish.
Se ele muda a versão da API no Go e tenta parar, o hook o devolve ao trabalho:
crossweft check fails:
- join:api-version disagrees: web@web/src/api.ts:3='2026-09-01'; api@server/main.go:14='2026-10-01'
[server/main.go:14] (key: join:api-version:b5dfb7e5)
A seam is out of agreement: bring the other side in line with the one you changed
(`crossweft impact <file>` names it).
O conselho depende do tipo de problema. Se um literal ao qual o mapa aponta desapareceu, o agente é orientado a atualizar a âncora ou restaurar o código, e não a "consertar o outro lado". A chave termina em um digest de qual arquivo guarda qual valor, então uma exceção registrada só desculpa exatamente aquele desacordo.
Um stop hook tem dois modos clássicos de falha: um loop sem fim e uma passagem silenciosa. O nosso só manda o agente de volta enquanto ele está progredindo (o conjunto de chaves que falham muda), no máximo quatro vezes seguidas. Sem progresso, o agente pode parar, e a pessoa recebe uma mensagem explícita de que a verificação continua vermelha.
Para o Claude Code há um plug-in com hooks e uma skill. Para o Codex há um plug-in de skill. Qualquer agente que fale MCP pode conectar crossweft mcp e obter a verificação, o impacto de mudanças e o "qual arquivo reler" como ferramentas somente leitura. Os adaptadores de hook para Gemini CLI, Copilot e Cursor ainda são experimentais: o formato de saída é testado, o ciclo completo ainda não.
Verificações que não podem passar em silêncio
A terceira história foi um validador que não escaneou nada durante semanas. Nem toda regra é "A igual a B"; às vezes você precisa provar que uma constante tem uma única fonte. Regras assim vivem em pequenos scripts, e são justamente os que quebram em silêncio.
O executor crossweft validators reprova um validador que sai com código diferente de zero, que não imprime SCANNED: files=<n> items=<n> ou imprime zeros, que não tem autoteste ou falha nele (SELF-TEST: checks=<n>, rodado contra um erro plantado), ou que esgota o tempo. Um validador que realmente não se aplica imprime APPLICABILITY: <reason> e recebe SKIP, não PASS.
O próprio Crossweft funciona do mesmo jeito. Há três resultados, não dois: 0 significa que tudo concorda, 1 significa desvio, 2 significa que a verificação não aconteceu (o modelo não pode ser lido, nada foi escaneado). Um código de saída 2 nunca parece sucesso.
Um registro de desvios conhecidos que não apodrece
Nem todo desacordo pode ser corrigido no mesmo pull request. Então você registra um achado: a chave do problema, um responsável e um próximo passo. Enquanto o problema existir, a verificação passa. Quando for corrigido, o achado fica obsoleto e reprova a verificação até que alguém o feche. Âncoras quebradas não podem ser desculpadas dessa forma: o mapa não tem permissão para descrever o código de forma errada.
Para um repositório que já tem desvios, crossweft baseline registra tudo como achados abertos com um responsável. A verificação fica verde, desvios novos ainda a reprovam, e o que for corrigido pede para ser fechado.
O que mudou
Um contexto honesto antes de tudo. Desde meados de julho, nosso instalador nunca conseguiu subir o serviço de sistema numa máquina virtual limpa. Adicionamos costuras verificadas em 26–28 de setembro, e em 29 de setembro o serviço subiu pela primeira vez. Isso é uma coincidência no tempo, não prova de causa: outras correções entraram ao mesmo tempo.
O que mudou de fato foi o tipo de falha. Três das primeiras falhas de 28 de setembro foram costuras: código de saída 76 (o nome do serviço em três cópias); o servidor recusando um loader que se registrou com a versão do artefato mas foi pedido com a versão do protocolo; e HTTP 500 porque dois caminhos de publicação removiam o pacote antigo e o terceiro não. Cada uma agora tem seu próprio guard. Os dois problemas seguintes foram bugs comuns, não "um valor em vários lugares".
O que o mapa mostra, recontado a partir do histórico do repositório:
| 27 set | 29 set | 4 out | |
|---|---|---|---|
| Blocos / elos | 130 / 160 | 134 / 166 | 138 / 192 |
| guards join | 10 | 220 (529 pontos) | 261 (644 pontos) |
| guards set | 4 | 125 | 138 |
| Achados | 58, todos abertos | 106: 93 fechados, 10 abertos, 3 aceitos | 146: 117 fechados, 25 abertos, 4 aceitos |
A primeira passada sobre "todo elo declara seu enforcement" foi feita pelos próprios agentes. Em um dia, eles acrescentaram 185 guards join e 107 guards set e encontraram 15 desacordos reais no código. Dos 93 achados fechados, 91 têm um commit de correção, e 58 dessas mensagens de commit citam o id do achado no mapa.
Um mapa sem verificação fica desatualizado em uma semana. Uma semana depois da primeira medição havia 96 problemas, e 94 deles eram o mapa ficando para trás, não bugs no código. Por isso o mapa é verificado toda vez que um agente para. Passamos de 4 validadores em junho para 50, e cada um precisa provar que escaneou alguma coisa.
Abrindo o código, e o que a primeira execução de CI pegou
A ferramenta cresceu dentro de um produto comercial, então a abrimos com cuidado. Há dois branches: o oss público, que é só a ferramenta, e um internal privado, que é o oss mais uma pasta com nossas próprias regras. As mudanças fluem do oss para o internal, nunca o contrário. O que vai ao público é um snapshot da árvore, um commit por release com uma assinatura neutra, não nosso histórico. Antes de publicar, um scanner verifica o snapshot e os metadados dos commits contra uma lista de palavras privadas, e um revisor independente e novo lê o diff desde a última release, porque um scanner só conhece as palavras que lhe deram. A primeira revisão encontrou três frases na documentação que insinuavam como nosso produto é construído, e nós as substituímos antes da release.
Uma tag de release só é criada depois que o CI fica verde em Linux, Windows e macOS, porque tags são imutáveis. A primeira execução foi vermelha. Ela pegou dois bugs reais que 157 testes locais não tinham visto:
- o código usava um argumento que só existe a partir do Python 3.10, enquanto prometemos 3.9;
- um plug-in editado duas vezes dentro do mesmo segundo, para o mesmo tamanho, rodava a partir de bytecode antigo, porque o cache do Python diferencia arquivos apenas por segundos e tamanho. Não conseguíamos ver isso localmente: a escrita de bytecode está desligada em toda a nossa máquina de desenvolvimento.
Corrigimos os dois, a segunda execução ficou verde, e a v0.1.0 saiu. Dois dias depois, a v0.2.0 acrescentou o servidor MCP, check --changed, guards construídos a partir de schemas OpenAPI e proto, e scanners de rota para mais cinco frameworks. Isso mostra o ponto deste artigo: uma verificação verde só informa sobre os tipos de erro que alguém já imaginou.
Isso já não existe?
Cada peça existe em algum lugar. O que não encontramos foi a combinação, e a regra de que todo elo declara como se mantém em acordo e toda costura escrita à mão tem um guard.
| Ferramenta | O que faz | Em que difere |
|---|---|---|
Google LINT.IfChange, ifttt-lint, if-changed |
"se este bloco muda, mude também aquele arquivo" | verifica que o diff tocou o arquivo; um join compara valores em toda execução |
| clevis | um valor igual em vários arquivos | sem conjuntos, sem regiões, sem mapa |
| Pact, buf, oasdiff | testes de contrato e compatibilidade de schema | a resposta certa quando existe um schema; o Crossweft constrói guards a partir de OpenAPI e proto para o lado escrito à mão |
| ArchUnit, dependency-cruiser, import-linter | regras de arquitetura | dentro de uma linguagem |
| archagent | invariantes de arquitetura e skills para agentes | o mais próximo em espírito, mas trata de estrutura, não de valores entre linguagens |
| GitNexus, codegraph | grafos de código e impacto de mudanças para agentes, via MCP | um grafo que você consulta, não uma verificação que reprova; os dois se complementam |
| fiberplane/drift, Swimm | documentação ancorada ao código | um hash falha em qualquer edição; uma âncora falha só quando o fato em si desaparece |
A abordagem também não é nossa. Birgitta Böckeler escreve sobre engenharia de harness: guias que agem antes de o agente escrever código e sensores que verificam depois, verificações determinísticas versus verificações feitas por um modelo de linguagem. A OpenAI escreve sobre o mesmo tema em Harness engineering: leveraging Codex in an agent-first world. O Crossweft é um sensor determinístico para costuras entre componentes, cuja saída diz ao agente exatamente o que corrigir.
Limitações
- Expressões regulares, não parsing. Reformate uma declaração e a verificação falha alto; você atualiza o padrão. Mas um padrão amplo demais ainda pode deixar passar um desacordo real, então olhe os valores extraídos e teste o comportamento em execução com testes comuns.
- Uma pessoa ainda confere se o enforcement é honesto. Os agentes montam o mapa, e o Crossweft não deixa que inventem um elo nem pulem uma pasta, mas se um elo é de fato
shared-codeou duas cópias às vezes é algo que só um desenvolvedor sabe. - A adoção dá trabalho. Num projeto pequeno o ganho é pequeno. Compensa onde há muitas linguagens e muitos processos.
- Até agora somos os únicos usuários. O ciclo do Claude Code está provado de ponta a ponta; outros agentes se conectam via MCP e pela skill, e os adaptadores de hook deles são experimentais.
check --changedacelera o pré-commit, mas ainda não quando a edição toca um roteador ou um arquivo de cliente.
Experimente
A forma mais rápida de ver o mecanismo é quebrar a demo (uma API em Go, um cliente TypeScript, um worker em Python):
pip install "git+https://github.com/AnastasiyaW/[email protected]"
git clone --branch v0.2.8 --depth 1 https://github.com/AnastasiyaW/crossweft
cd crossweft/examples/polyglot-shop && crossweft check # RESULT: PASS
# now change API_VERSION in web/src/api.ts only:
crossweft check # join:api-version disagrees
Cada linha abaixo foi verificada numa cópia da demo: a verificação falha com código de saída 1 e nomeia o guard.
| Mudança | Pego por |
|---|---|
alterar API_VERSION só em web/src/api.ts |
join:api-version |
adicionar um campo JSON só na struct Order do Go |
set:order-fields |
| adicionar um status só no worker Python | set:worker-statuses |
ler uma nova variável de ambiente no worker que não está no .env.example |
set:env-worker-vars |
chamar /v1/refunds a partir do cliente |
route:client-unexplained |
mudar o arredondamento em server/pricing.go |
pair:price-rounding |
No seu próprio repositório: crossweft init (init --example mostra uma costura funcionando primeiro), depois peça ao seu agente para mapear o repositório; a skill guia o processo e crossweft check mantém tudo honesto. No Claude Code: /plugin marketplace add AnastasiyaW/crossweft, depois /plugin install crossweft@crossweft. No CI: uses: AnastasiyaW/[email protected], ou crossweft check --format sarif para o code scanning do GitHub.
O repositório do Crossweft verifica as próprias costuras com o Crossweft: a versão em três arquivos, os eventos de hook, os comandos na documentação. Enquanto escrevíamos o README, o guard "comandos documentados existem" disparou três vezes em linhas de prosa que ele tomou por comandos. Corrigimos essas também.
O Crossweft é Apache-2.0. Issues e pull requests são bem-vindos em github.com/AnastasiyaW/crossweft.